diff --git a/CMakeLists.txt b/CMakeLists.txt index 72dc941aa..a8127ceea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,6 +553,7 @@ IF(USE_TESTS) test/block_tests.c test/buffer_tests.c test/chacha20_tests.c + test/checkpoints_tests.c test/context_tests.c test/cstr_tests.c test/ecc_tests.c diff --git a/Makefile.am b/Makefile.am index 14dc6d8ca..f3a483dc8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -280,6 +280,7 @@ tests_SOURCES = \ test/block_tests.c \ test/buffer_tests.c \ test/chacha20_tests.c \ + test/checkpoints_tests.c \ test/context_tests.c \ test/cstr_tests.c \ test/ecc_tests.c \ diff --git a/include/dogecoin/chainparams.h b/include/dogecoin/chainparams.h index 87d6a1b10..f39d96500 100644 --- a/include/dogecoin/chainparams.h +++ b/include/dogecoin/chainparams.h @@ -68,8 +68,10 @@ extern const dogecoin_chainparams dogecoin_chainparams_test; extern const dogecoin_chainparams dogecoin_chainparams_regtest; // the mainnet checkpoints, needs a fix size -extern const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[33]; -extern const dogecoin_checkpoint dogecoin_testnet_checkpoint_array[24]; +extern const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[]; +extern const size_t dogecoin_mainnet_checkpoint_count; +extern const dogecoin_checkpoint dogecoin_testnet_checkpoint_array[]; +extern const size_t dogecoin_testnet_checkpoint_count; LIBDOGECOIN_API const dogecoin_chainparams* chain_from_b58_prefix(const char* address); LIBDOGECOIN_API int chain_from_b58_prefix_bool(char* address); diff --git a/include/dogecoin/headersdb_file.h b/include/dogecoin/headersdb_file.h index cc38e8d7b..3dd9e771c 100644 --- a/include/dogecoin/headersdb_file.h +++ b/include/dogecoin/headersdb_file.h @@ -55,6 +55,8 @@ typedef struct dogecoin_headers_db_ dogecoin_blockindex genesis; dogecoin_blockindex *chaintip; dogecoin_blockindex *chainbottom; + dogecoin_bool batch_write; /**< Suppress per-record fdatasync; caller must commit after */ + dogecoin_bool skip_pow; /**< Skip scrypt PoW verify (for checkpoint-anchored bulk loads) */ } dogecoin_headers_db; dogecoin_headers_db *dogecoin_headers_db_new(const dogecoin_chainparams* chainparams, dogecoin_bool inmem_only); diff --git a/include/dogecoin/libdogecoin.h b/include/dogecoin/libdogecoin.h index 89a3ba56b..16ae60051 100644 --- a/include/dogecoin/libdogecoin.h +++ b/include/dogecoin/libdogecoin.h @@ -112,8 +112,10 @@ extern const dogecoin_chainparams dogecoin_chainparams_test; extern const dogecoin_chainparams dogecoin_chainparams_regtest; // the mainnet checkpoints, needs a fix size -extern const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[33]; -extern const dogecoin_checkpoint dogecoin_testnet_checkpoint_array[24]; +extern const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[]; +extern const size_t dogecoin_mainnet_checkpoint_count; +extern const dogecoin_checkpoint dogecoin_testnet_checkpoint_array[]; +extern const size_t dogecoin_testnet_checkpoint_count; const dogecoin_chainparams* chain_from_b58_prefix(const char* address); int chain_from_b58_prefix_bool(char* address); diff --git a/include/dogecoin/spv.h b/include/dogecoin/spv.h index ba2bbdf49..42ff74b60 100644 --- a/include/dogecoin/spv.h +++ b/include/dogecoin/spv.h @@ -54,12 +54,63 @@ typedef struct spv_block_sample_ uint64_t fees; // total fees } spv_block_sample; +#define PAR_HDR_RAW_LEN 80 + +/* How many times a segment may fail to connect at flush before the parallel + * downloader gives up entirely and lets the sequential path take over. Each + * retry re-requests the segment from whatever the primary DB actually reached, + * usually from a different peer. */ +#define PAR_HDR_MAX_FLUSH_FAILS 3 + +/* One parallel header-download segment. Each segment spans the open-closed + * height interval (start_height, stop_height] and is assigned to one node. */ +typedef struct par_hdr_seg_ { + uint32_t start_height; /* height of start_hash (exclusive lower bound) */ + uint256_t start_hash; /* block hash at start_height — getheaders locator */ + uint32_t stop_height; /* height of stop_hash (inclusive upper bound) */ + uint256_t stop_hash; /* block hash at stop_height — getheaders hash_stop */ + + /* download progress */ + int node_id; /* assigned node (-1 = unassigned) */ + int shadow_id; /* second node racing this segment (-1 = none) */ + uint64_t shadow_at; /* time the shadow was attached */ + uint64_t requested_at; /* time of the last getheaders sent for this seg */ + uint64_t assigned_at; /* time the current owner took this segment */ + uint32_t count_at_assign;/* headers already buffered when it took it */ + uint32_t tip_height; /* highest header received so far in this segment */ + uint256_t tip_hash; /* hash of that header (next-batch locator) */ + + /* buffered raw 80-byte block headers in ascending height order */ + uint8_t *buf; + uint32_t count; /* headers buffered */ + uint32_t cap; /* buffer capacity */ + + dogecoin_bool complete; /* all stop_height - start_height headers received */ + dogecoin_bool flushed; /* segment has been flushed into the primary DB */ + uint32_t flush_fails; /* times this segment failed to connect at flush */ +} par_hdr_seg; + +/* Top-level state for a parallel genesis header download. */ +typedef struct par_hdr_state_ { + par_hdr_seg *segs; /* ordered array of segments */ + uint32_t num_segs; /* total segment count */ + uint32_t flush_idx; /* index of the next segment pending flush */ + uint32_t last_flush_idx; /* flush_idx at the last observed progress */ + uint64_t last_progress_time; /* time of that progress */ + uint64_t buffered_bytes; /* raw header bytes staged across segments */ + uint32_t rate_ref; /* last median peer rate seen while >=3 segs + * were in flight; the tail has no crowd to + * compare against, so it compares to this */ + dogecoin_bool active; /* download in progress */ +} par_hdr_state; + typedef struct dogecoin_spv_client_ { dogecoin_node_group *nodegroup; uint64_t last_headersrequest_time; uint64_t oldest_item_of_interest; dogecoin_bool use_checkpoints; + struct par_hdr_state_ *par_hdr; /* parallel genesis header sync state */ const dogecoin_chainparams *chainparams; int stateflags; uint64_t last_statecheck_time; @@ -130,6 +181,8 @@ typedef struct dogecoin_spv_client_ void* zk_pending_commits; } dogecoin_spv_client; +LIBDOGECOIN_API dogecoin_bool dogecoin_spv_client_enable_genesis_headers(dogecoin_spv_client *client); + LIBDOGECOIN_API dogecoin_spv_client* dogecoin_spv_client_new(const dogecoin_chainparams *params, dogecoin_bool debug, dogecoin_bool headers_memonly, dogecoin_bool use_checkpoints, dogecoin_bool full_sync, int maxnodes, const char *http_server); LIBDOGECOIN_API void dogecoin_spv_client_free(dogecoin_spv_client *client); LIBDOGECOIN_API dogecoin_bool dogecoin_spv_client_load(dogecoin_spv_client *client, const char *file_path, dogecoin_bool prompt); diff --git a/src/chainparams.c b/src/chainparams.c index d7183195e..f56ca1528 100644 --- a/src/chainparams.c +++ b/src/chainparams.c @@ -88,29 +88,83 @@ const dogecoin_chainparams dogecoin_chainparams_regtest = { const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[] = { {0, "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691", 1386325540, 0x1e0ffff0}, + {100000, "13ab3b961fcc500c03f51279385c42e9f055d48a37dfa72d0073c0d3f595036b", 1392346781, 0x1b267eeb}, {104679, "35eb87ae90d44b98898fec8c39577b76cb1eb08e1261cfc10706c8ce9a1d01cf", 1392637497, 0x1b41676b}, {145000, "cc47cae70d7c5c92828d3214a266331dde59087d4a39071fa76ddfff9b7bde72", 1395094679, 0x1b499dfd}, + {200000, "092fd3e76db5ff35fbfefe48d5c53ca26e799f0654a4036ddd5fd78de77418c2", 1398695540, 0x1b42fb92}, + {300000, "7c2b3b8cb1629fa014a29e85cd7b0fe7876ce3def8899ba4c0945c0de2ad7902", 1405442852, 0x1b2e13bb}, {371337, "60323982f9c5ff1b5a954eac9dc1269352835f47c2c5222691d80f0d50dcf053", 1410464577, 0x1b364184}, {450000, "d279277f8f846a224d776450aa04da3cf978991a182c6f3075db4c48b173bbd7", 1415413000, 0x1b03efda}, + {500000, "92ae1083b7b3c22fd7b4ce2eae121d518f8e1aa81d0be9432ce5aa20a2954fcc", 1418570422, 0x1b04f203}, + {600000, "caa5446c05c8e51cf7985a6cb4da4dc2b730e4ec399dc95b2a094df6bdd4f268", 1424881308, 0x1b03f04d}, + {700000, "eac853ae22d59a498386241a3de69a36739ccc9e0a6acfd617b64c5ea4a0f4b3", 1431191204, 0x1b05d766}, {771275, "1b7d789ed82cbdc640952e7e7a54966c6488a32eaad54fc39dff83f310dbaaed", 1435666139, 0x1b0444d2}, + {800000, "773fbb34e1bfe82467eb24cda8769dfdcd13a5b4dac4c8f9f6534c40301f7fbf", 1437473213, 0x1b06b43a}, + {900000, "5235e97ba5ae95c04300daed8d75dca2ed6eef9be42d433d985d3203639a4a66", 1443761100, 0x1b028fe5}, {1000000, "6aae55bea74235f0c80bd066349d4440c31f2d0f27d54265ecd484d8c1d11b47", 1450031952, 0x1b02dcf8}, + {1100000, "d48fef240e48d9395d08c2ec4fe9c6d5554091a107e1c0fddedf5db3a435d96e", 1456288963, 0x1b033eb4}, + {1200000, "1f8ef813b31ec896e3f7f064d6637e251242ba95c1bb12ca339e55b0614a53db", 1462554308, 0x1b036f1b}, {1250000, "00c7a442055c1a990e11eea5371ca5c1c02a0677b33cc88ec728c45edc4ec060", 1465690401, 0x1b045d00}, + {1300000, "80be4067b5dc8e3db9b787ca4a69c09b3478b6434edc51bc8984db1a79fa620f", 1468822277, 0x1b02fe44}, + {1400000, "18c7078848109e3d95706f639f1501550765e4675a02ddb8bfe3a703bb49fb8f", 1475060460, 0x1b039462}, {1500000, "f1d32d6920de7b617d51e74bdf4e58adccaa582ffdc8657464454f16a952fca6", 1481313507, 0x1b0306ef}, + {1600000, "ca5eb72f1e0d160f1481f74d56d7cc4a27d91aa585ba012da8018a5fe934d61b", 1487574651, 0x1b01b66a}, + {1700000, "c151a40f121a4f0ee0078e0268563c8299ad12652f939d9c6880aab9a93c1969", 1493860971, 0x1b016434}, {1750000, "5c8e7327984f0d6f59447d89d143e5f6eafc524c82ad95d176c5cec082ae2001", 1496985750, 0x1b00d4d4}, + {1800000, "c90be6e4d4dfd4f6cfa37e17afe2cb423d82a1af726f402244b4607d155bd021", 1500113228, 0x1b009c6d}, + {1900000, "6ab174805b47f1c93d69fbd37876375cacf57c075b3039561ddb05759f0853c0", 1506361934, 0x1a46b9c8}, {2000000, "9914f0e82e39bbf21950792e8816620d71b9965bdbbc14e72a95e3ab9618fea8", 1512600918, 0x1a1c1225}, {2031142, "893297d89afb7599a3c571ca31a3b80e8353f4cf39872400ad0f57d26c4c5d42", 1514549787, 0x1a15d633}, + {2100000, "4f95b8f837f139f512dd8ba26fe4dd702271394daccb29007dc52938c96ccf85", 1518833207, 0x1a08f167}, + {2200000, "7027f0e32f0566f39367580dfb9d42157608fc7ce4483bf2471beb68b3b3f26e", 1525088854, 0x1a07034c}, {2250000, "0a87a8d4e40dca52763f93812a288741806380cd569537039ee927045c6bc338", 1528215255, 0x1a053477}, + {2300000, "2f391edf30851d6ab71e1100a907d314190a15764da600e478730369db422f93", 1531345450, 0x1a04439c}, + {2400000, "41a17417233ec887dfd5950c731e64abfab5fe230353d226a6a3de580e59c3bf", 1537593624, 0x1a059c04}, + {2500000, "3352c854b77d6946fabed44b4451bc998a48dbbcc2dbc1d313f47b0e8a782d18", 1543848050, 0x1a05d53a}, {2510150, "77e3f4a4bcb4a2c15e8015525e3d15b466f6c022f6ca82698f329edef7d9777e", 1544484077, 0x1a0868af}, + {2600000, "37bd4bb2b345ca3bc7035adf8850632159d965878f7ab59fbcb4ffd382895a03", 1550097077, 0x1a05f4f3}, + {2700000, "e76b5a769f05c868c831e067946ce32221ad3e163b4bfd96ae2745233587e7cd", 1556337998, 0x1a03ef42}, {2750000, "d4f8abb835930d3c4f92ca718aaa09bef545076bd872354e0b2b85deefacf2e3", 1559459044, 0x1a0292e2}, + {2800000, "5af3d9109a9c3a75179a9a85f385ca5e045a40dcb4f716ad907beae3d5ee662b", 1562584608, 0x1a0336c7}, + {2900000, "7bc6c24835317d4944a468a27a112eae5d564fc53cf366e34c8c7452f30e09e5", 1568837724, 0x1a045469}, {3000000, "195a83b091fb3ee7ecb56f2e63d01709293f57f971ccf373d93890c8dc1033db", 1575096781, 0x1a07da10}, + {3100000, "c7970179a8433e85b13a4930f80367a05a50c1f4b8878b31cb4b0ca7506f04c1", 1581365323, 0x1a0a5662}, + {3200000, "1026822a0313b2466ddc13c251dd98072430708cc53c56e43e71dd9ab0625437", 1587656622, 0x1a0786ac}, {3250000, "7f3e28bf9e309c4b57a4b70aa64d3b2ea5250ae797af84976ddc420d49684034", 1590799169, 0x1a05bfa3}, + {3300000, "76f7f333faf010117d9ca7dd0c6200456dcbffa03b31dee66541966347589b8b", 1593941349, 0x1a050d2a}, + {3400000, "20588e8f8ce89cab1f894ef80faf935c631df24229a1a42bafee781d9a57a454", 1600234275, 0x1a04bf31}, {3500000, "eaa303b93c1c64d2b3a2cdcf6ccf21b10cc36626965cc2619661e8e1879abdfb", 1606543340, 0x1a08d505}, + {3600000, "3319474fec2b5cbc57df8a8b976fbbb4907d5315b23943daf58bc6ce0bba9347", 1612832557, 0x1a03ecc0}, {3606083, "954c7c66dee51f0a3fb1edb26200b735f5275fe54d9505c76ebd2bcabac36f1e", 1613218169, 0x1a03d764}, + {3700000, "d0f0af23aadcf6b8d4a681ee930e39d1e64aca967187fa8a0c655c6dacfa22ce", 1619169876, 0x1a043028}, + {3800000, "0e1a1b524408f5d3e23fc461351daf9469976679e37cfc321ab2e06a3cb4f01a", 1625505238, 0x1a03d014}, {3854173, "e4b4ecda4c022406c502a247c0525480268ce7abbbef632796e8ca1646425e75", 1628934997, 0x1a03ca36}, + {3900000, "3815e66fe50a8c9770c04306626fa6f6be7a087a5e03c0b014ffee470dbb215e", 1631833148, 0x1a032198}, {3963597, "2b6927cfaa5e82353d45f02be8aadd3bfd165ece5ce24b9bfa4db20432befb5d", 1635884460, 0x1a037bc9}, + {4000000, "47f227cd54c270aa18c5136635fc003a11cb82d6f8319dd8dab180a2f5555a9e", 1638207091, 0x1a0350f2}, + {4100000, "8112b8a11dd3d1965cab6ccc0175abca62ec6b1225ad303e72e94efac635007e", 1644587549, 0x1a02450e}, + {4200000, "93da94c346ff5299ff400be4c1008aae99e9b70ef74db22820e42049ce14c6b4", 1650989758, 0x1a03efe7}, + {4300000, "337d3b4bf937e2f9513e064b58253c9d0f188b7b928d570b07c03651d47e250f", 1657392921, 0x1a0461ea}, {4303965, "ed7d266dcbd8bb8af80f9ccb8deb3e18f9cc3f6972912680feeb37b090f8cee0", 1657646310, 0x1a0344f5}, + {4400000, "367bdf79d7c527a3f79430d72cf62a8201ba52edd608410a6da1b1ab7311e04d", 1663792227, 0x1a028e13}, + {4500000, "66d27fdeb0f694d06d1276b2d3d72c4dbefe294651ddc1ade220951823243396", 1670200610, 0x1a020cfc}, + {4600000, "75323db271259899e66cbf4cfab33dcc034527e34bdc75b9a70e1b2deec1aad1", 1676603347, 0x1a016fd5}, + {4700000, "748844093ba9fd6ba002ed2d0c9857c0292f1fb4aeb949d2fa8f0d438a18387f", 1682991374, 0x1a014ec5}, + {4800000, "a1e58e27594bbc04f9c021c9f73771852260fce5e09e86071d34c74a8a116d01", 1689360368, 0x1a01b637}, + {4900000, "8bc08384ae8f17e0891159f3cd5783c07a62bd37c3a67d8d5e1bd292addced46", 1695779125, 0x1a02888f}, + {5000000, "5ae6eadfe5fd98fa7f388b671c4863db4b62e46eecc054465a276d419500e469", 1702188235, 0x1a010be6}, {5050000, "e7d4577405223918491477db725a393bcfc349d8ee63b0a4fde23cbfbfd81dea", 1705383360, 0x1a019541}, + {5100000, "a60110809b403aecd06185ec73753ddfd21dbc243e82c5ef76c88648d1b7b718", 1708571081, 0x1a01114a}, + {5200000, "06381a6241f120951a2f751851f50320e059b084ebc639c6b9d7d8b076d1f0bc", 1714956041, 0x1a01a709}, + {5300000, "8ccc7ebe3d4a97bfdf1993145114f654c537a9b06bc89bb12beabc954fb3110f", 1721347471, 0x1a00f17c}, {5400000, "cbb1f4ae807da83e13bdf9c28188982938c9ee6bf560c1023f51adac229eef87", 1727704957, 0x0106DAC9}, + {5500000, "a2c15e69513051e6c01cce388718efa7a803f52766e6dc91624812e32250c56e", 1734026997, 0x1a00d1c5}, + {5600000, "39c02091aa7b8ad483a87957b12bdf4c7c6ebcc5dfe2c4648c4d235b40961c8d", 1740360175, 0x1a0093c1}, + {5700000, "28ef0c3aa091c839048bba16bcee4df442961e2bc4e42bda407906f71e5b594d", 1746679266, 0x1a00adee}, + {5800000, "8d1540c92ec87451d73573fec3720ca7e835e630538096e3e11c56dec8205e2e", 1753007429, 0x1a00d815}, + {5900000, "9eb4809b6bf358a5bf9fbddf82da9ff4f047ef76577d2939efabefc7bd6ced07", 1759340677, 0x1a00801a}, + {6000000, "7af46caeb390c15e5d92b4aa58854b55351b43fe5fc714e359b31d0ce019a187", 1765676839, 0x192fa398}, + {6100000, "bdeff3efe99a8203ad2baae74e50c29f5158227ff03d0bb7ffa3ff9fbd25dee3", 1772013218, 0x1a0082ee}, {6148124, "0f4d009c402553dbec50012ace7e224018595e4b788d1af78cea72eb6401d959", 1775065954, 0x1a00b3ec}, {6154988, "8ca6972c34a2154b6ae6f15996de8d1a794477ec0add439ee1279bd9a21b2eed", 1775506602, 0x1a008107}, {6156000, "e8257a8929faef98929dc4e006bf405ec0fdfa93cb29b6d336a04e6b4d7bd02e", 1775571096, 0x1a0088a2}, @@ -119,7 +173,17 @@ const dogecoin_checkpoint dogecoin_mainnet_checkpoint_array[] = { {6173706, "1a59af827274d479127434728f9e1885a45b9a252bab246d039045822e671aa5", 1776698026, 0x197935DD}, {6189700, "a6ed6d6b03668d0bb9baddfbc52565b2514d02f118d6d0298a6ca48d511d7804", 1777713485, 0x19429592}, {6191600, "a8eeeadc81e8def69167f0d82c50c4d05fdb0c6ffd17a6af955f80d6925e0f0d", 1777835169, 0x197c3bc0}, - {6201360, "0bb1dbb971969f89f1c4c6305396042de1d6a390d71a44b066255c844da9256c", 1778454835, 0x19555f03}}; + {6201360, "0bb1dbb971969f89f1c4c6305396042de1d6a390d71a44b066255c844da9256c", 1778454835, 0x19555f03}, + {6250000, "197f31497b1e3b506da36eb6126738e3206e42fdff240844f8b75ae4ebc29519", 1781539362, 0x19614D63}, + {6275000, "63c89f44fe3ce35d6abaa297d32d75826ce4368027e098f816432f68c2bedbd6", 1783120876, 0x1979F333}, + {6300000, "e35560d7a6dda44da8fd5d3fea4025319ac5d6287dc3c1308cde650e51e12e58", 1784704739, 0x19671029}}; + +/* Counts defined next to the arrays. sizeof() on the extern declarations + yields whatever bound the header states, not the real length: chainparams.h + said [87] and libdogecoin.h said [33] while this file defined 89, so callers + silently saw a truncated array. */ +const size_t dogecoin_mainnet_checkpoint_count = + sizeof(dogecoin_mainnet_checkpoint_array) / sizeof(dogecoin_mainnet_checkpoint_array[0]); const dogecoin_checkpoint dogecoin_testnet_checkpoint_array[] = { {0, "bb0a78264637406b6360aad926284d544d7049f45189db5664f3c4d07350559e", 1391503289, 0x1e0ffff0}, @@ -209,3 +273,6 @@ dogecoin_bool isMainnetFromB58Prefix(const char address[P2PKHLEN]) { /* Check if chainparams is mainnet */ return (chainparams == &dogecoin_chainparams_main); } + +const size_t dogecoin_testnet_checkpoint_count = + sizeof(dogecoin_testnet_checkpoint_array) / sizeof(dogecoin_testnet_checkpoint_array[0]); diff --git a/src/cli/spvnode.c b/src/cli/spvnode.c index e21e82051..a83dd511d 100644 --- a/src/cli/spvnode.c +++ b/src/cli/spvnode.c @@ -200,6 +200,7 @@ static struct option long_options[] = { {"filtered_blocks", no_argument, NULL, 'g'}, {"select_checkpoint", no_argument, NULL, 'q'}, {"daemon", no_argument, NULL, 'z'}, + {"genesis_headers", no_argument, NULL, 'H'}, {NULL, 0, NULL, 0} }; /** @@ -216,7 +217,7 @@ static void print_usage() { print_version(); printf("Usage: spvnode (-c|continuous) (-i|--ips ) (-m[--maxpeers] ) (-f ) \ (-a|--address
) (-n|--mnemonic ) (-s|[--pass_phrase]) (-y|--encrypted_file ) \ -(-w|--wallet_file ) (-h|--headers_file ) (-l|[--no_prompt]) (-b[--full_sync]) (-p[--checkpoint]) (-k[--master_key]) (-j[--use_tpm]) \ +(-w|--wallet_file ) (-h|--headers_file ) (-l|[--no_prompt]) (-b[--full_sync]) (-H[--genesis_headers]) (-p[--checkpoint]) (-k[--master_key]) (-j[--use_tpm]) \ (-u|--http_server ) (-x|--smpv) (-g|--filtered_blocks) (-q|--select_checkpoint) (-t|--testnet) (-r|--regtest) (-d|--debug) \n"); printf("Supported commands:\n"); printf(" scan (scan blocks up to the tip, creates header.db file)\n"); @@ -296,10 +297,10 @@ static int spv_choose_checkpoint_index(const dogecoin_chainparams* chain, dogeco if (chain == &dogecoin_chainparams_main) { checkpoints = dogecoin_mainnet_checkpoint_array; - count = (int)(sizeof(dogecoin_mainnet_checkpoint_array) / sizeof(dogecoin_mainnet_checkpoint_array[0])); + count = (int)(dogecoin_mainnet_checkpoint_count); } else if (chain == &dogecoin_chainparams_test) { checkpoints = dogecoin_testnet_checkpoint_array; - count = (int)(sizeof(dogecoin_testnet_checkpoint_array) / sizeof(dogecoin_testnet_checkpoint_array[0])); + count = (int)(dogecoin_testnet_checkpoint_count); } else { return -1; } @@ -404,10 +405,10 @@ void spv_sync_completed(dogecoin_spv_client* client) { if (client->chainparams == &dogecoin_chainparams_main) { checkpoints = dogecoin_mainnet_checkpoint_array; - checkpoint_count = (int)(sizeof(dogecoin_mainnet_checkpoint_array) / sizeof(dogecoin_mainnet_checkpoint_array[0])); + checkpoint_count = (int)(dogecoin_mainnet_checkpoint_count); } else if (client->chainparams == &dogecoin_chainparams_test) { checkpoints = dogecoin_testnet_checkpoint_array; - checkpoint_count = (int)(sizeof(dogecoin_testnet_checkpoint_array) / sizeof(dogecoin_testnet_checkpoint_array[0])); + checkpoint_count = (int)(dogecoin_testnet_checkpoint_count); } if (checkpoints && checkpoint_count > 0) { suggested_checkpoint_height = (int)checkpoints[0].height; @@ -470,6 +471,7 @@ int main(int argc, char* argv[]) { char* name = 0; char* headers_name = 0; dogecoin_bool full_sync = false; + dogecoin_bool genesis_headers = false; dogecoin_bool have_decl_daemon = false; dogecoin_bool prompt = true; dogecoin_bool encrypted = false; @@ -487,7 +489,7 @@ int main(int argc, char* argv[]) { data = argv[argc - 1]; /* get arguments */ - while ((opt = getopt_long_only(argc, argv, "i:ctrdsm:n:f:y:u:w:h:a:lbpzkjxgq", long_options, &long_index)) != -1) { + while ((opt = getopt_long_only(argc, argv, "i:ctrdsm:n:f:y:u:w:h:a:lbpzkjxgqH", long_options, &long_index)) != -1) { switch (opt) { case 'c': quit_when_synced = false; @@ -522,6 +524,9 @@ int main(int argc, char* argv[]) { case 'b': full_sync = true; break; + case 'H': + genesis_headers = true; + break; case 'p': use_checkpoint = true; break; @@ -715,6 +720,14 @@ int main(int argc, char* argv[]) { printf("Filtered block mode disabled (use -g/--filtered_blocks to enable)\n"); } + if (genesis_headers) { + if (!dogecoin_spv_client_enable_genesis_headers(client)) + printf("[par-hdr] warning: no checkpoints for this chain; genesis header sync unavailable\n"); + else + printf("[par-hdr] parallel genesis header download enabled (%u segments)\n", + (unsigned int)client->par_hdr->num_segs); + } + client->sync_transaction = dogecoin_wallet_check_transaction; client->sync_transaction_ctx = wallet; #endif diff --git a/src/headersdb_file.c b/src/headersdb_file.c index a851c413c..c9f07deb3 100644 --- a/src/headersdb_file.c +++ b/src/headersdb_file.c @@ -285,7 +285,8 @@ dogecoin_bool dogecoin_headers_db_write(dogecoin_headers_db* db, dogecoin_blocki ser_u256(rec, arith_to_uint256(&blockindex->chainwork)); dogecoin_block_header_serialize(rec, &blockindex->header); size_t res = fwrite(rec->str, rec->len, 1, db->headers_tree_file); - dogecoin_file_commit(db->headers_tree_file); + if (!db->batch_write) + dogecoin_file_commit(db->headers_tree_file); cstr_free(rec, true); return (res == 1); } @@ -340,14 +341,16 @@ dogecoin_blockindex * dogecoin_headers_db_connect_hdr(dogecoin_headers_db* db, s if (connect_at != NULL) { // Check the proof of work if (!is_auxpow(blockindex->header.version)) { - uint256_t hash = {0}; - cstring* s = cstr_new_sz(64); - dogecoin_block_header_serialize(s, (const dogecoin_block_header*) &blockindex->header); - dogecoin_block_header_scrypt_hash(s, &hash); - cstr_free(s, true); - if (!check_pow(&hash, blockindex->header.bits, db->params, &blockindex->chainwork)) { - printf("%s:%d:%s : non-AUX proof of work failed : %s\n", __FILE__, __LINE__, __func__, strerror(errno)); - return blockindex; + if (!db->skip_pow) { + uint256_t hash = {0}; + cstring* s = cstr_new_sz(64); + dogecoin_block_header_serialize(s, (const dogecoin_block_header*) &blockindex->header); + dogecoin_block_header_scrypt_hash(s, &hash); + cstr_free(s, true); + if (!check_pow(&hash, blockindex->header.bits, db->params, &blockindex->chainwork)) { + printf("%s:%d:%s : non-AUX proof of work failed : %s\n", __FILE__, __LINE__, __func__, strerror(errno)); + return blockindex; + } } } diff --git a/src/spv.c b/src/spv.c index ffab9d4a9..079f1d81e 100644 --- a/src/spv.c +++ b/src/spv.c @@ -187,6 +187,22 @@ static dogecoin_bool spv_log_merkle_match(const uint8_t txid[32], uint32_t pos, } static const unsigned int HEADERS_MAX_RESPONSE_TIME = 120; +/* Seconds a parallel header segment may sit without a getheaders response + before it is released back to the pool for another peer to pick up. */ +static const uint64_t PAR_HDR_SEG_TIMEOUT = 120; +/* Seconds without the ordered flush advancing before we log loudly. */ +static const uint64_t PAR_HDR_STALL_WARN = 300; +/* Ceiling on raw header bytes staged in memory ahead of the ordered flush. + Segment sizes are uneven (the checkpoint arrays were merged at different + intervals), so a fixed segment count bounds neither memory nor peer + utilisation well; budget the thing that actually grows. */ +static const uint64_t PAR_HDR_MAX_BUFFERED = 256ull * 1024 * 1024; +/* A segment must have been owned this long before its download rate is judged; + below this the sample is noise. */ +static const uint64_t PAR_HDR_RATE_GRACE = 30; +/* The flush-head owner is preempted when its rate falls below this fraction of + the median rate across the other active segments, expressed as a divisor. */ +static const uint32_t PAR_HDR_SLOW_FACTOR = 4; static const unsigned int MIN_TIME_DELTA_FOR_STATE_CHECK = 5; static const unsigned int BLOCK_GAP_TO_DEDUCT_TO_START_SCAN_FROM = 5; static const unsigned int BLOCKS_DELTA_IN_S = 60; @@ -426,6 +442,11 @@ static int spv_zk_buffer_pending(dogecoin_spv_client* client, static dogecoin_bool dogecoin_net_spv_node_timer_callback(dogecoin_node *node, uint64_t *now); void dogecoin_net_spv_post_cmd(dogecoin_node *node, dogecoin_p2p_msg_hdr *hdr, struct const_buffer *buf); void dogecoin_net_spv_node_handshake_done(dogecoin_node *node); +void par_hdr_assign(dogecoin_spv_client *client, dogecoin_node *node); +void par_hdr_reclaim(dogecoin_spv_client *client, uint64_t now); +void par_hdr_free(dogecoin_spv_client *client); +static void par_hdr_recv(dogecoin_spv_client *client, dogecoin_node *node, + struct const_buffer *buf, uint32_t count); void dogecoin_node_connection_state_changed_cb(dogecoin_node *node) { if (node->nodegroup->should_connect_to_more_nodes_cb) { @@ -653,6 +674,8 @@ void dogecoin_spv_client_free(dogecoin_spv_client *client) client->merkle_match_active = false; client->merkle_match_blockindex = NULL; + + par_hdr_free(client); if (client->headers_db) { if (client->headers_db_ctx) @@ -770,6 +793,10 @@ void dogecoin_net_spv_periodic_statecheck(dogecoin_node *node, uint64_t *now) // tick regardless of how much time had actually elapsed. They are updated // only when an actual request is sent (see dogecoin_net_spv_request_headers // and the getheaders/getdata send paths). + /* Parallel genesis header sync: reclaim segments from dead or silent + peers and top up idle peers before the normal request path runs. */ + par_hdr_reclaim(client, *now); + if ((client->stateflags & SPV_HEADER_SYNC_FLAG) == SPV_HEADER_SYNC_FLAG) { dogecoin_net_spv_request_headers(client); @@ -823,8 +850,8 @@ void dogecoin_net_spv_fill_block_locator(dogecoin_spv_client *client, vector_t * if (client->use_checkpoints && client->oldest_item_of_interest > BLOCK_GAP_TO_DEDUCT_TO_START_SCAN_FROM * BLOCKS_DELTA_IN_S) { dogecoin_bool is_main = (client->chainparams && strcmp(client->chainparams->chainname, "main") == 0); const dogecoin_checkpoint *checkpoint = is_main ? dogecoin_mainnet_checkpoint_array : dogecoin_testnet_checkpoint_array; - size_t mainnet_checkpoint_size = sizeof(dogecoin_mainnet_checkpoint_array) / sizeof(dogecoin_mainnet_checkpoint_array[0]); - size_t testnet_checkpoint_size = sizeof(dogecoin_testnet_checkpoint_array) / sizeof(dogecoin_testnet_checkpoint_array[0]); + size_t mainnet_checkpoint_size = dogecoin_mainnet_checkpoint_count; + size_t testnet_checkpoint_size = dogecoin_testnet_checkpoint_count; size_t length = is_main ? mainnet_checkpoint_size : testnet_checkpoint_size; int i; for (i = (int)length - 1; i >= 0; i--) { @@ -857,6 +884,7 @@ void dogecoin_net_spv_fill_block_locator(dogecoin_spv_client *client, vector_t * * @param node The node that is requesting headers or blocks. * @param blocks boolean, true if we want to request blocks, false if we want to request headers */ + void dogecoin_net_spv_node_request_headers_or_blocks(dogecoin_node *node, dogecoin_bool blocks) { // request next headers @@ -893,6 +921,9 @@ void dogecoin_net_spv_node_request_headers_or_blocks(dogecoin_node *node, dogeco */ dogecoin_bool dogecoin_net_spv_request_headers(dogecoin_spv_client *client) { + /* Parallel genesis headers in progress -- don't interfere. */ + if (client->par_hdr && client->par_hdr->active) return true; + size_t i; dogecoin_bool new_headers_available = false; for(i = 0; i < client->nodegroup->nodes->len; ++i) @@ -992,7 +1023,10 @@ void dogecoin_net_spv_node_handshake_done(dogecoin_node *node) } } - dogecoin_net_spv_request_headers((dogecoin_spv_client*)node->nodegroup->ctx); + if (client && client->par_hdr && client->par_hdr->active) + par_hdr_assign(client, node); + else + dogecoin_net_spv_request_headers((dogecoin_spv_client*)node->nodegroup->ctx); } /** @@ -1692,7 +1726,21 @@ void dogecoin_net_spv_post_cmd(dogecoin_node *node, dogecoin_p2p_msg_hdr *hdr, s uint32_t amount_of_headers; if (!deser_varlen(&amount_of_headers, buf)) return; uint64_t now = time(NULL); - client->nodegroup->log_write_cb("Got %d headers (took %d s) from node %d\n", amount_of_headers, now - client->last_headersrequest_time, node->nodeid); + /* last_headersrequest_time is only armed by the serial path; in + parallel mode it stays 0 and the subtraction printed a raw epoch. + Deliberately not armed here — it drives a per-node stall check that + would misbehave every peer at once if the whole sync went quiet. */ + if (client->par_hdr && client->par_hdr->active) + client->nodegroup->log_write_cb("Got %d headers from node %d\n", + amount_of_headers, node->nodeid); + else + client->nodegroup->log_write_cb("Got %d headers (took %d s) from node %d\n", amount_of_headers, now - client->last_headersrequest_time, node->nodeid); + + /* Parallel genesis headers mode -- buffer into the owning segment. */ + if (client->par_hdr && client->par_hdr->active) { + par_hdr_recv(client, node, buf, amount_of_headers); + return; + } // flag off the request stall check client->last_headersrequest_time = 0; @@ -2563,3 +2611,1062 @@ LIBDOGECOIN_API dogecoin_bool dogecoin_spv_client_filterclear(dogecoin_spv_clien return true; } + + + +static uint32_t spv_elapsed(const dogecoin_spv_client *client) { + return (uint32_t)((uint64_t)time(NULL) - client->start_ts); +} + +/* ================================================================ */ +/* Parallel genesis header download */ +/* ================================================================ */ + +/* Build segments from the chainparams block-header checkpoint array. + * Segment i covers the open-closed height range (prev_checkpoint, checkpoint[i]]. + * Segment 0 starts from the genesis block hash (height 0). + * Returns a newly allocated par_hdr_state, or NULL for chains with no checkpoints. */ +static par_hdr_state *par_hdr_init(const dogecoin_chainparams *params) +{ + const dogecoin_checkpoint *arr = NULL; + size_t cnt = 0; + if (strcmp(params->chainname, "main") == 0) { + arr = dogecoin_mainnet_checkpoint_array; + cnt = dogecoin_mainnet_checkpoint_count; + } else if (strcmp(params->chainname, "test") == 0) { + arr = dogecoin_testnet_checkpoint_array; + cnt = dogecoin_testnet_checkpoint_count; + } + if (!cnt) return NULL; + + /* arr[0] is the genesis entry (height=0) — it is the start anchor for the + * first segment, not an endpoint to download toward. Skip it so segment 0 + * covers the range genesis..arr[1] instead of the degenerate 0..0 range. */ + size_t start_i = (cnt > 0 && arr[0].height == 0) ? 1 : 0; + size_t num_segs = cnt - start_i; + if (!num_segs) return NULL; + + par_hdr_state *s = dogecoin_calloc(1, sizeof(par_hdr_state)); + s->segs = dogecoin_calloc(num_segs, sizeof(par_hdr_seg)); + s->num_segs = (uint32_t)num_segs; + s->active = true; + s->last_flush_idx = 0; + s->last_progress_time = (uint64_t)time(NULL); + + for (uint32_t i = 0; i < (uint32_t)num_segs; i++) { + uint32_t arr_i = (uint32_t)(start_i + i); + par_hdr_seg *seg = &s->segs[i]; + + /* stop = checkpoint[arr_i] */ + seg->stop_height = arr[arr_i].height; + utils_uint256_sethex((char *)arr[arr_i].hash, seg->stop_hash); + + + /* start = genesis when i==0, else checkpoint[arr_i-1] */ + if (i == 0) { + seg->start_height = 0; + memcpy(seg->start_hash, params->genesisblockhash, sizeof(uint256_t)); + } else { + seg->start_height = arr[arr_i - 1].height; + utils_uint256_sethex((char *)arr[arr_i - 1].hash, seg->start_hash); + } + + seg->node_id = -1; + seg->shadow_id = -1; + seg->shadow_at = 0; + seg->tip_height = seg->start_height; + memcpy(seg->tip_hash, seg->start_hash, sizeof(uint256_t)); + + seg->cap = 2048; + seg->buf = dogecoin_malloc((size_t)seg->cap * PAR_HDR_RAW_LEN); + } + return s; +} + +/* Send a getheaders request to @node for the next batch in @seg. */ +static dogecoin_node *par_hdr_node_by_id(dogecoin_spv_client *client, int node_id); + +static void par_hdr_send_getheaders(dogecoin_node *node, par_hdr_seg *seg) +{ + seg->requested_at = (uint64_t)time(NULL); + + vector_t *locators = vector_new(1, free); + uint256_t *loc = dogecoin_calloc(1, sizeof(uint256_t)); + memcpy(loc, seg->tip_hash, sizeof(uint256_t)); + vector_add(locators, loc); + + cstring *msg = cstr_new_sz(512); + dogecoin_p2p_msg_getheaders(locators, (uint8_t *)seg->stop_hash, msg); + vector_free(locators, true); + + cstring *p2p = dogecoin_p2p_message_new( + node->nodegroup->chainparams->netmagic, + DOGECOIN_MSG_GETHEADERS, msg->str, msg->len); + cstr_free(msg, true); + dogecoin_node_send(node, p2p); + cstr_free(p2p, true); + + node->state |= NODE_HEADERSYNC; +} + +/* Assign the next unassigned segment to @node and send the first getheaders. */ +LIBDOGECOIN_API void par_hdr_assign(dogecoin_spv_client *client, dogecoin_node *node) +{ + par_hdr_state *s = client->par_hdr; + if (!s || !s->active) return; + if (!(node->state & NODE_CONNECTED) || !node->version_handshake) return; + + /* Skip nodes already working on a segment */ + for (uint32_t i = 0; i < s->num_segs; i++) { + if (s->segs[i].node_id == (int)node->nodeid || + s->segs[i].shadow_id == (int)node->nodeid) return; + } + + /* Lowest-index segment that is neither complete nor currently owned. A + released segment keeps its buffered headers and tip_hash, so a new owner + resumes where the previous one stopped rather than restarting. */ + par_hdr_seg *seg = NULL; + uint32_t seg_idx = 0; + for (uint32_t i = s->flush_idx; i < s->num_segs; i++) { + if (!s->segs[i].complete && s->segs[i].node_id == -1) { + /* The flush head is always allowed: refusing it would deadlock, + since nothing can drain while it is unowned. */ + if (i != s->flush_idx && s->buffered_bytes >= PAR_HDR_MAX_BUFFERED) + return; + seg = &s->segs[i]; + seg_idx = i; + break; + } + } + if (!seg) return; /* nothing outstanding */ + + seg->node_id = (int)node->nodeid; + seg->assigned_at = (uint64_t)time(NULL); + seg->count_at_assign = seg->count; + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] assigned node %d to segment %u (heights %u..%u)\n", + node->nodeid, seg_idx, + seg->tip_height + 1, seg->stop_height); + + par_hdr_send_getheaders(node, seg); +} + +/* Flush completed segments (in order) into the primary headers DB. + * Returns the number of segments flushed. */ +/* Check the invariants the segment array is supposed to hold, and log any that + * do not. Returns the number of violations, 0 when healthy. + * + * Deliberately not assert(). This codebase compiles with NDEBUG in release, so + * an assert here would document the invariants while checking them only in + * builds nobody ships. These conditions are cheap -- one pass over ~89 + * segments -- and the failures they describe are silent ones, which is exactly + * the case for paying at runtime. + * + * Called after the operations that mutate segment ownership or flush state. + * It reports rather than aborts: a violation means a bug in this file, and + * killing a node mid-sync is a worse outcome than finishing with a diagnostic + * in the log. */ +static uint32_t par_hdr_check(dogecoin_spv_client *client, const char *where) +{ + par_hdr_state *s = client->par_hdr; + uint32_t bad = 0; + uint32_t i; + + if (!s) return 0; + +#define PAR_HDR_BAD(fmt, ...) \ + do { \ + bad++; \ + if (client->nodegroup && client->nodegroup->log_write_cb) \ + client->nodegroup->log_write_cb("[par-hdr][INVARIANT] %s: " fmt, \ + where, __VA_ARGS__); \ + } while (0) + + /* flush_idx only ever moves forward, and never past the end. */ + if (s->flush_idx > s->num_segs) + PAR_HDR_BAD("flush_idx %u exceeds num_segs %u\n", + s->flush_idx, s->num_segs); + + for (i = 0; i < s->num_segs; i++) { + const par_hdr_seg *seg = &s->segs[i]; + + /* A segment below flush_idx has been written to the DB; one at or + * above it has not. Ordered flushing is what lets the downloader skip + * proof-of-work, so a hole here breaks the anchoring argument. */ + if (i < s->flush_idx && !seg->flushed) + PAR_HDR_BAD("segment %u is below flush_idx %u but not flushed\n", + i, s->flush_idx); + if (i >= s->flush_idx && seg->flushed) + PAR_HDR_BAD("segment %u is at/above flush_idx %u but flushed\n", + i, s->flush_idx); + + /* Flushed implies complete, and implies the staging buffer is gone -- + * that release is what keeps peak RSS off the size of the chain. */ + if (seg->flushed && !seg->complete) + PAR_HDR_BAD("segment %u flushed but not complete\n", i); + if (seg->flushed && seg->buf) + PAR_HDR_BAD("segment %u flushed but still holds its buffer\n", i); + + /* A completed segment is owned by nobody: par_hdr_assign only + * considers !complete && node_id == -1, so a complete segment with an + * owner leaks that peer out of the assignment pool for the rest of the + * run. */ + if (seg->complete && seg->node_id != -1) + PAR_HDR_BAD("segment %u complete but still owned by node %d\n", + i, seg->node_id); + if (seg->complete && seg->shadow_id != -1) + PAR_HDR_BAD("segment %u complete but still shadowed by node %d\n", + i, seg->shadow_id); + + /* A shadow races the owner; it is meaningless without one, and a peer + * cannot race itself. */ + if (seg->shadow_id != -1 && seg->node_id == -1) + PAR_HDR_BAD("segment %u has shadow %d but no owner\n", + i, seg->shadow_id); + if (seg->shadow_id != -1 && seg->shadow_id == seg->node_id) + PAR_HDR_BAD("segment %u shadowed by its own owner %d\n", + i, seg->node_id); + + /* Heights bracket the range this segment is responsible for. */ + if (seg->stop_height <= seg->start_height) + PAR_HDR_BAD("segment %u stop %u is not above start %u\n", + i, seg->stop_height, seg->start_height); + if (seg->tip_height < seg->start_height) + PAR_HDR_BAD("segment %u tip %u is below start %u\n", + i, seg->tip_height, seg->start_height); + + /* count is the number of headers staged in buf -- but only until the + * segment is flushed, after which the buffer is released and count is + * kept as the record of how many headers went in. So this pair only + * describes unflushed segments. */ + if (!seg->flushed) { + if (seg->count > 0 && !seg->buf) + PAR_HDR_BAD("segment %u claims %u staged headers with no buffer\n", + i, seg->count); + if (seg->count > seg->cap) + PAR_HDR_BAD("segment %u count %u exceeds capacity %u\n", + i, seg->count, seg->cap); + } + + /* One peer, one segment. Two segments owned by the same node means + * par_hdr_recv cannot tell which one a batch belongs to -- it matches + * on node_id and takes the first. */ + if (seg->node_id != -1) { + uint32_t j; + for (j = i + 1; j < s->num_segs; j++) { + if (s->segs[j].complete) continue; + if (s->segs[j].node_id == seg->node_id) + PAR_HDR_BAD("node %d owns both segment %u and %u\n", + seg->node_id, i, j); + } + } + } + +#undef PAR_HDR_BAD + + return bad; +} + +static uint32_t par_hdr_flush(dogecoin_spv_client *client) +{ + par_hdr_state *s = client->par_hdr; + uint32_t flushed = 0; + + /* Batch-optimise writes: suppress per-record fdatasync and skip scrypt + * PoW verification. Segments are checkpoint-anchored so the chain is + * implicitly validated. A single fsync at the end suffices. */ + dogecoin_headers_db *hdb = (dogecoin_headers_db *)client->headers_db_ctx; + dogecoin_bool prev_batch = hdb ? hdb->batch_write : false; + dogecoin_bool prev_skip = hdb ? hdb->skip_pow : false; + if (hdb) { hdb->batch_write = true; hdb->skip_pow = true; } + + uint32_t flush_idx_before = s->flush_idx; + + while (s->flush_idx < s->num_segs && s->segs[s->flush_idx].complete && + !s->segs[s->flush_idx].flushed) { + par_hdr_seg *seg = &s->segs[s->flush_idx]; + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] flushing segment %u (%u headers, heights %u..%u)\n", + s->flush_idx, seg->count, + seg->start_height + 1, seg->stop_height); + + uint32_t bad = 0; + for (uint32_t j = 0; j < seg->count; j++) { + struct const_buffer cbuf = { + (const void *)(seg->buf + (size_t)j * PAR_HDR_RAW_LEN), + PAR_HDR_RAW_LEN + }; + dogecoin_bool connected; + dogecoin_blockindex *pindex = + client->headers_db->connect_hdr(client->headers_db_ctx, &cbuf, false, &connected); + if (!connected) { + if (bad == 0 && client->nodegroup && client->nodegroup->log_write_cb) { + /* Decode prev_block from the raw header for diagnostics */ + const uint8_t *raw = seg->buf + (size_t)j * PAR_HDR_RAW_LEN; + /* Standard header layout: version(4) + prev_block(32) */ + char prev_hex[65] = {0}; + for (int _k = 0; _k < 32; _k++) + snprintf(prev_hex + _k*2, 3, "%02x", raw[4 + (31-_k)]); + char tip_hex[65] = {0}; + if (hdb && hdb->chaintip) + for (int _k = 0; _k < 32; _k++) + snprintf(tip_hex + _k*2, 3, "%02x", ((uint8_t*)hdb->chaintip->hash)[_k]); + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: first connect failure at j=%u\n" + " chaintip height=%d hash=%s\n" + " prev_block in hdr=%s\n", + s->flush_idx, j, + hdb && hdb->chaintip ? (int)hdb->chaintip->height : -1, tip_hex, + prev_hex); + } + bad++; + dogecoin_free(pindex); /* orphan — not in DB */ + /* Stop at the first failure. Header j+1 chains off header j, + * which is not in the DB, so nothing after this point can + * connect either -- continuing would allocate and free an + * orphan per remaining header and blur where the chain broke. */ + break; + } else { + if (pindex && client->header_connected) + client->header_connected(client); + /* pindex is now db->chaintip — owned by the DB, do NOT free */ + } + } + + if (bad) { + /* Do NOT mark this segment flushed or advance flush_idx. The old + * code did both unconditionally and freed the staging buffer, so a + * mid-segment connect failure lost those headers permanently, left + * a hole in the chain, and let every later segment fail the same + * way until flush_idx ran off the end and the sync reported "all + * segments complete" over a broken chain. + * + * Instead, resume the segment from what the DB actually reached. + * Headers 0..j-1 did connect, so the chaintip is the correct + * locator; re-requesting from start_hash would replay those and + * fail immediately. The buffer is dropped and the segment is put + * back up for assignment, usually landing on a different peer. */ + seg->flush_fails++; + + uint64_t staged = (uint64_t)seg->count * PAR_HDR_RAW_LEN; + s->buffered_bytes = (s->buffered_bytes > staged) + ? s->buffered_bytes - staged : 0; + dogecoin_free(seg->buf); + seg->buf = NULL; + seg->cap = 0; + seg->count = 0; + + if (hdb && hdb->chaintip) { + memcpy(seg->tip_hash, hdb->chaintip->hash, DOGECOIN_HASH_LENGTH); + seg->tip_height = (uint32_t)hdb->chaintip->height; + } else { + memcpy(seg->tip_hash, seg->start_hash, DOGECOIN_HASH_LENGTH); + seg->tip_height = seg->start_height; + } + + seg->complete = false; + seg->node_id = -1; + seg->shadow_id = -1; + seg->shadow_at = 0; + seg->count_at_assign = 0; + + if (seg->flush_fails >= PAR_HDR_MAX_FLUSH_FAILS) { + /* Retrying has not helped, so the range is not merely a bad + * peer. Hand the rest of the chain to the sequential path, + * which verifies AUXPoW and can make progress from the real + * chaintip without trusting segment boundaries. */ + s->active = false; + + /* Segments past flush_idx may be complete and still holding + * their staging buffers. Nothing will flush them now, and on + * mainnet that is most of the chain resident for the rest of + * the run, so release them here rather than at teardown. */ + for (uint32_t k = s->flush_idx + 1; k < s->num_segs; k++) { + if (!s->segs[k].buf) continue; + uint64_t held = (uint64_t)s->segs[k].count * PAR_HDR_RAW_LEN; + s->buffered_bytes = (s->buffered_bytes > held) + ? s->buffered_bytes - held : 0; + dogecoin_free(s->segs[k].buf); + s->segs[k].buf = NULL; + s->segs[k].cap = 0; + s->segs[k].count = 0; + } + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u failed to connect %u times — " + "disabling parallel download, falling back to " + "sequential from height %u\n", + s->flush_idx, seg->flush_fails, seg->tip_height); + } else if (client->nodegroup && client->nodegroup->log_write_cb) { + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: connect failed, re-requesting from " + "height %u (attempt %u of %u)\n", + s->flush_idx, seg->tip_height, + seg->flush_fails, (uint32_t)PAR_HDR_MAX_FLUSH_FAILS); + } + break; + } + + seg->flushed = true; + s->flush_idx++; + flushed++; + + /* The headers are on disk now — release the staging buffer. Without + this every flushed segment keeps its raw headers resident until + par_hdr_free at teardown, which on mainnet is the whole chain. */ + uint64_t freed = (uint64_t)seg->count * PAR_HDR_RAW_LEN; + s->buffered_bytes = (s->buffered_bytes > freed) + ? s->buffered_bytes - freed : 0; + dogecoin_free(seg->buf); + seg->buf = NULL; + seg->cap = 0; + } + + if (hdb) { + hdb->batch_write = prev_batch; + hdb->skip_pow = prev_skip; + if (flushed > 0 && hdb->headers_tree_file) + dogecoin_file_commit(hdb->headers_tree_file); + } + if (s->flush_idx != flush_idx_before) { + s->last_flush_idx = s->flush_idx; + s->last_progress_time = (uint64_t)time(NULL); + } + + par_hdr_check(client, "after flush"); + + return flushed; +} + +/* Advance @buf past the AUXPoW chain data that follows the 80-byte standard + * header in a P2P headers message for AUXPoW blocks (version & 0x100). + * Mirrors deserialize_dogecoin_auxpow_block's buffer consumption but skips + * check_auxpow — PoW is guaranteed by checkpoint anchors at segment boundaries. */ +static dogecoin_bool par_hdr_skip_auxpow(struct const_buffer *buf) { + /* parent coinbase tx (variable length) */ + size_t cb_len = 0; + dogecoin_tx *dummy = dogecoin_tx_new(); + dogecoin_bool ok = (dogecoin_bool)dogecoin_tx_deserialize(buf->p, buf->len, dummy, &cb_len); + dogecoin_tx_free(dummy); + if (!ok || cb_len == 0 || !deser_skip(buf, cb_len)) return false; + + /* parent_hash (32 bytes) */ + if (!deser_skip(buf, 32)) return false; + + /* parent merkle branch: count (varint) + count×32 bytes */ + uint32_t merkle_count = 0; + if (!deser_varlen(&merkle_count, buf)) return false; + if (merkle_count > 0 && !deser_skip(buf, (size_t)merkle_count * 32)) return false; + + /* parent_merkle_index (uint32) */ + if (!deser_skip(buf, 4)) return false; + + /* aux merkle branch: count (varint) + count×32 bytes */ + uint32_t aux_count = 0; + if (!deser_varlen(&aux_count, buf)) return false; + if (aux_count > 0 && !deser_skip(buf, (size_t)aux_count * 32)) return false; + + /* aux_merkle_index (uint32) */ + if (!deser_skip(buf, 4)) return false; + + /* parent block header: version(4) + prev_block(32) + merkle_root(32) + time(4) + bits(4) + nonce(4) */ + if (!deser_skip(buf, 80)) return false; + + return true; +} + +/* Called from the DOGECOIN_MSG_HEADERS handler when par_hdr is active. + * @buf points just past the varint that gave @count. */ +static void par_hdr_recv(dogecoin_spv_client *client, dogecoin_node *node, + struct const_buffer *buf, uint32_t count) +{ + par_hdr_state *s = client->par_hdr; + + /* Find the segment assigned to this node */ + par_hdr_seg *seg = NULL; + uint32_t seg_idx = 0; + for (uint32_t i = 0; i < s->num_segs; i++) { + if (s->segs[i].complete) continue; + if (s->segs[i].node_id == (int)node->nodeid) { + seg = &s->segs[i]; + seg_idx = i; + break; + } + /* A racing shadow delivering before the owner takes the segment over. + * Whichever peer answers first is by definition the faster one on this + * range, and the buffered headers are shared, so the swap costs + * nothing already downloaded. */ + if (s->segs[i].shadow_id == (int)node->nodeid) { + dogecoin_node *old_owner = par_hdr_node_by_id(client, s->segs[i].node_id); + if (old_owner) old_owner->state &= ~NODE_HEADERSYNC; + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: shadow node %d won the race from node %d\n", + i, (int)node->nodeid, s->segs[i].node_id); + s->segs[i].node_id = (int)node->nodeid; + s->segs[i].shadow_id = -1; + s->segs[i].shadow_at = 0; + s->segs[i].assigned_at = (uint64_t)time(NULL); + s->segs[i].count_at_assign = s->segs[i].count; + seg = &s->segs[i]; + seg_idx = i; + break; + } + } + if (!seg) { + /* Unsolicited response — discard */ + node->state &= ~NODE_HEADERSYNC; + return; + } + + /* Buffer each raw 80-byte standard header. + * + * AUXPoW blocks (version & 0x100) carry variable-length AUXPoW chain data + * between the standard 80-byte header and the 1-byte tx_count varint in the + * P2P headers message. We copy the standard 80 bytes, advance buf by 80, + * then call par_hdr_skip_auxpow to consume the AUXPoW data without running + * check_auxpow (checkpoint anchors at segment boundaries guarantee validity). */ + /* A batch must continue from what this segment already holds. + * + * Two peers can be attached to one segment while a race is resolving, and + * a getheaders is answered relative to the locator sent at request time. + * The loser's reply can therefore arrive after the winner has advanced the + * segment, carrying headers that start below seg->tip_height. Appending + * those at seg->count leaves a discontinuity in the middle of the buffer + * that only surfaces at flush, as a connect failure. + * + * Compare the first header's prev_block against the hash this segment + * expects next and drop the whole batch if it does not match. Worth doing + * even without racing: it also rejects a peer that answers with something + * other than the continuation it was asked for. */ + if (count > 0 && buf->len >= PAR_HDR_RAW_LEN) { + const uint8_t *first_prev = (const uint8_t *)buf->p + 4; /* version(4) */ + /* tip_hash is always the right anchor: it is seeded from start_hash when + * the segment is built, advanced per header received, and re-seeded from + * the primary DB chaintip when a flush fails. Selecting start_hash on + * count == 0 instead would be wrong in that last case -- the segment has + * no buffered headers but the DB is already past start_height, so every + * correctly-served batch would be rejected and the segment would be + * re-requested forever. */ + const uint8_t *expected = (const uint8_t *)seg->tip_hash; + if (memcmp(first_prev, expected, DOGECOIN_HASH_LENGTH) != 0) { + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: dropping %u headers from node %d, " + "does not continue from height %u\n", + seg_idx, count, (int)node->nodeid, seg->tip_height); + return; + } + } + + for (uint32_t i = 0; i < count; i++) { + if (buf->len < PAR_HDR_RAW_LEN) break; + + /* Peek at version to detect AUXPoW (wire format: little-endian int32) */ + uint32_t wire_ver; + memcpy(&wire_ver, buf->p, 4); + const dogecoin_bool is_aux = (le32toh(wire_ver) & 0x100) != 0; + + /* Grow segment buffer if needed */ + if (seg->count >= seg->cap) { + /* cap can be 0 if the buffer was released after a flush; a + flushed segment should never be written to again, but doubling + 0 would silently never grow. */ + seg->cap = seg->cap ? seg->cap * 2 : 2048; + seg->buf = dogecoin_realloc(seg->buf, (size_t)seg->cap * PAR_HDR_RAW_LEN); + } + + /* Copy the standard 80-byte header and advance past it */ + s->buffered_bytes += PAR_HDR_RAW_LEN; + memcpy(seg->buf + (size_t)seg->count * PAR_HDR_RAW_LEN, buf->p, PAR_HDR_RAW_LEN); + buf->p = (const uint8_t *)buf->p + PAR_HDR_RAW_LEN; + buf->len -= PAR_HDR_RAW_LEN; + + /* For AUXPoW blocks, skip the variable-length chain data */ + if (is_aux && !par_hdr_skip_auxpow(buf)) break; + + /* Compute header hash from the buffered 80 bytes */ + dogecoin_block_header hdr; + struct const_buffer hbuf = { seg->buf + (size_t)seg->count * PAR_HDR_RAW_LEN, + PAR_HDR_RAW_LEN }; + if (!dogecoin_block_header_deserialize(&hdr, &hbuf, client->chainparams, NULL)) { + /* An 80-byte header that will not deserialize cannot advance the + * continuity anchor. Counting it anyway would leave tip_hash and + * tip_height describing header n-1 while seg->count moved to n, so + * the next batch's prev_block check would compare against the wrong + * hash and the discontinuity would only surface at flush, as a + * connect failure well away from its cause. Drop the remainder of + * the batch and let the segment be re-requested from tip_hash. */ + s->buffered_bytes -= PAR_HDR_RAW_LEN; + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: undeserializable header at index %u " + "from node %d, dropping rest of batch\n", + seg_idx, seg->count, (int)node->nodeid); + break; + } + dogecoin_block_header_hash(&hdr, (uint8_t *)seg->tip_hash); + seg->tip_height++; + seg->count++; + + /* skip tx_count varint (always 0x00 in headers messages) */ + if (buf->len > 0) { buf->p = (const uint8_t *)buf->p + 1; buf->len--; } + } + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] seg %u: buffered %u, tip_height=%u, stop=%u\n", + seg_idx, seg->count, seg->tip_height, seg->stop_height); + + if (seg->tip_height >= seg->stop_height) { + /* The segment claims to have reached its checkpoint. Verify that it + * actually landed on it before trusting the range. + * + * stop_hash comes from the checkpoint array and is sent as getheaders + * hash_stop, but nothing compared it against what arrived. Safety was + * emergent: a divergent segment left a chaintip the *next* segment + * could not extend, so it surfaced one segment late, as a connect + * failure away from its cause -- and the final segment has no next + * segment, so nothing caught it at all. + * + * This is the whole justification for skipping proof-of-work between + * checkpoints, so it is worth checking directly rather than inferring. + * The segment has never been flushed at this point (flush only touches + * complete && !flushed segments, in order), so rejecting it costs + * nothing already written. */ + if (memcmp(seg->tip_hash, seg->stop_hash, DOGECOIN_HASH_LENGTH) != 0) { + dogecoin_headers_db *hdb = + (dogecoin_headers_db *)client->headers_db_ctx; + + seg->flush_fails++; + + char got_hex[65] = {0}, want_hex[65] = {0}; + for (int _k = 0; _k < 32; _k++) { + snprintf(got_hex + _k*2, 3, "%02x", ((const uint8_t *)seg->tip_hash)[31-_k]); + snprintf(want_hex + _k*2, 3, "%02x", ((const uint8_t *)seg->stop_hash)[31-_k]); + } + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u: terminal hash mismatch at height %u " + "from node %d (attempt %u)\n" + " expected %s\n" + " got %s\n", + seg_idx, seg->tip_height, (int)node->nodeid, + seg->flush_fails, want_hex, got_hex); + + uint64_t staged = (uint64_t)seg->count * PAR_HDR_RAW_LEN; + s->buffered_bytes = (s->buffered_bytes > staged) + ? s->buffered_bytes - staged : 0; + dogecoin_free(seg->buf); + seg->buf = NULL; + seg->cap = 0; + seg->count = 0; + + /* Resume from the primary DB if it is already inside this segment + * -- that only happens when an earlier flush failed partway and + * left the chaintip mid-range -- otherwise from the checkpoint + * anchor, since nothing from this segment reached the DB. */ + if (hdb && hdb->chaintip && + (uint32_t)hdb->chaintip->height > seg->start_height && + (uint32_t)hdb->chaintip->height <= seg->stop_height) { + memcpy(seg->tip_hash, hdb->chaintip->hash, DOGECOIN_HASH_LENGTH); + seg->tip_height = (uint32_t)hdb->chaintip->height; + } else { + memcpy(seg->tip_hash, seg->start_hash, DOGECOIN_HASH_LENGTH); + seg->tip_height = seg->start_height; + } + + seg->complete = false; + seg->node_id = -1; + seg->shadow_id = -1; + seg->shadow_at = 0; + seg->count_at_assign = 0; + node->state &= ~NODE_HEADERSYNC; + + if (seg->flush_fails >= PAR_HDR_MAX_FLUSH_FAILS) { + s->active = false; + for (uint32_t k = 0; k < s->num_segs; k++) { + if (k == seg_idx || !s->segs[k].buf) continue; + uint64_t held = (uint64_t)s->segs[k].count * PAR_HDR_RAW_LEN; + s->buffered_bytes = (s->buffered_bytes > held) + ? s->buffered_bytes - held : 0; + dogecoin_free(s->segs[k].buf); + s->segs[k].buf = NULL; + s->segs[k].cap = 0; + s->segs[k].count = 0; + } + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u failed terminal check %u times — " + "disabling parallel download, falling back to " + "sequential from height %u\n", + seg_idx, seg->flush_fails, seg->tip_height); + } else { + par_hdr_assign(client, node); + } + return; + } + + /* Segment complete */ + seg->complete = true; + seg->node_id = -1; + seg->shadow_id = -1; + seg->shadow_at = 0; + node->state &= ~NODE_HEADERSYNC; + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] segment %u complete (%u headers)\n", seg_idx, seg->count); + + /* Try to flush as many ordered completed segments as possible */ + par_hdr_flush(client); + + /* Re-assign this node to the next segment */ + par_hdr_assign(client, node); + + par_hdr_check(client, "after segment complete"); + + /* Check if all segments are done */ + if (s->flush_idx >= s->num_segs) { + s->active = false; + dogecoin_blockindex *tip = + client->headers_db->getchaintip(client->headers_db_ctx); + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] all segments complete — primary DB height %d [%us elapsed]\n", + tip ? tip->height : -1, spv_elapsed(client)); + } + } else { + /* More headers needed for this segment */ + par_hdr_send_getheaders(node, seg); + } +} + +/* Look up a connected node by its nodeid, or NULL if it is gone. */ +static dogecoin_node *par_hdr_node_by_id(dogecoin_spv_client *client, int nodeid) +{ + if (!client || !client->nodegroup || !client->nodegroup->nodes) return NULL; + for (size_t i = 0; i < client->nodegroup->nodes->len; i++) { + dogecoin_node *n = vector_idx(client->nodegroup->nodes, i); + if (n && (int)n->nodeid == nodeid) return n; + } + return NULL; +} + +/* Headers per second delivered by the current owner of @seg, or 0 if the + * segment has not been owned long enough for the sample to mean anything. */ +static uint32_t par_hdr_seg_rate(const par_hdr_seg *seg, uint64_t now) +{ + if (seg->node_id == -1 || seg->assigned_at == 0) return 0; + if (now <= seg->assigned_at) return 0; + uint64_t elapsed = now - seg->assigned_at; + if (elapsed < PAR_HDR_RATE_GRACE) return 0; + if (seg->count <= seg->count_at_assign) return 0; + return (uint32_t)((seg->count - seg->count_at_assign) / elapsed); +} + +/* Release the flush-head segment when its owner is alive but far slower than + * its peers and someone else is free to take over. + * + * A dead owner is handled by the timeout in par_hdr_reclaim. A merely slow + * one is not: it keeps answering inside the deadline while every segment above + * it finishes into memory and their owners go idle, because the ordered flush + * cannot advance past the head. One slow peer therefore throttles the whole + * sync. Resuming from tip_hash means preemption costs nothing already + * downloaded. + */ + +/* Attach a second peer to an already-owned segment. + * + * Segments are the unit of parallelism and cannot be subdivided in flight, so + * once fewer segments remain than there are peers, the extra peers idle and + * the sync runs at the speed of whoever holds the last one. Preemption helps + * only if the owner is a clear outlier; a merely mediocre peer keeps the + * segment while faster ones sit unused. + * + * When there is genuine spare capacity -- strictly more idle peers than + * incomplete segments -- hand the segment to a second peer as well and take + * whichever reaches the stop height first. The cost is duplicated bandwidth on + * the last few segments; the benefit is that tail latency stops depending on + * which peer happened to draw the final segment. + */ +static void par_hdr_race_tail(dogecoin_spv_client *client, uint64_t now) +{ + par_hdr_state *s = client->par_hdr; + if (!client->nodegroup || !client->nodegroup->nodes) return; + + uint32_t incomplete = 0; + for (uint32_t i = 0; i < s->num_segs; i++) + if (!s->segs[i].complete) incomplete++; + if (incomplete == 0) return; + + /* Count peers not currently owning or shadowing anything. */ + uint32_t idle_count = 0; + for (size_t i = 0; i < client->nodegroup->nodes->len; i++) { + dogecoin_node *n = vector_idx(client->nodegroup->nodes, i); + if (!n || !(n->state & NODE_CONNECTED) || !n->version_handshake) continue; + dogecoin_bool busy = false; + for (uint32_t k = 0; k < s->num_segs && !busy; k++) + if (s->segs[k].node_id == (int)n->nodeid || + s->segs[k].shadow_id == (int)n->nodeid) busy = true; + if (!busy) idle_count++; + } + if (idle_count == 0) return; /* nobody free to help */ + + /* Racing used to require idle_count > incomplete, which meant it only + * engaged once nearly every segment was done. A full mainnet run showed + * that is far too late: the first race fired about three quarters of the + * way through the log, so every peer that finished early sat idle while + * mid-run segments crawled. Any idle peer is spare capacity; the question + * is only which segment is worth spending it on. + * + * Shadow a segment only when its owner is running below the reference + * rate. A peer keeping pace does not need help, and shadowing it would + * just duplicate bandwidth. Being merely below median is enough here -- + * preemption already handles the clear outliers, and this is the cheaper + * intervention because the original owner keeps working. */ + uint32_t ref = s->rate_ref; + if (ref == 0) { + /* No reference yet: sample the segments currently in flight. */ + uint32_t sum = 0, n = 0; + for (uint32_t i = 0; i < s->num_segs; i++) { + uint32_t r = par_hdr_seg_rate(&s->segs[i], now); + if (r > 0) { sum += r; n++; } + } + if (n == 0) return; + ref = sum / n; + } + + /* Shadow the segments nearest the flush head first: those gate everything. */ + for (uint32_t i = s->flush_idx; i < s->num_segs && idle_count > 0; i++) { + par_hdr_seg *seg = &s->segs[i]; + if (seg->complete || seg->node_id == -1 || seg->shadow_id != -1) continue; + + /* Leave owners that are keeping up alone. A rate of 0 means the owner + * is still inside the grace window, which also counts as no evidence + * of trouble. */ + uint32_t rate = par_hdr_seg_rate(seg, now); + if (rate == 0 || rate >= ref) continue; + + dogecoin_node *cand = NULL; + for (size_t j = 0; j < client->nodegroup->nodes->len && !cand; j++) { + dogecoin_node *n = vector_idx(client->nodegroup->nodes, j); + if (!n || !(n->state & NODE_CONNECTED) || !n->version_handshake) continue; + if ((int)n->nodeid == seg->node_id) continue; + dogecoin_bool busy = false; + for (uint32_t k = 0; k < s->num_segs && !busy; k++) + if (s->segs[k].node_id == (int)n->nodeid || + s->segs[k].shadow_id == (int)n->nodeid) busy = true; + if (!busy) cand = n; + } + if (!cand) return; + + seg->shadow_id = (int)cand->nodeid; + seg->shadow_at = now; + idle_count--; + + if (client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] racing segment %u: node %d shadowing node %d " + "(%u hdr/s vs ref %u, resuming at %u)\n", + i, seg->shadow_id, seg->node_id, rate, ref, + seg->tip_height + 1); + + par_hdr_send_getheaders(cand, seg); + } +} + +static void par_hdr_preempt_head(dogecoin_spv_client *client, uint64_t now) +{ + par_hdr_state *s = client->par_hdr; + if (s->flush_idx >= s->num_segs) return; + + par_hdr_seg *head = &s->segs[s->flush_idx]; + if (head->complete || head->node_id == -1) return; + + uint32_t head_rate = par_hdr_seg_rate(head, now); + if (head_rate == 0) return; /* still inside the grace window */ + + /* Median rate across the other segments currently being downloaded. + * + * Sized to num_segs rather than a fixed 64. The old bound stopped sampling + * at 64 without saying so, and mainnet is already at 89 segments, so once + * more than 64 are in flight the median is drawn from an arbitrary prefix + * of the peer set -- and that median is the reference every preemption and + * racing decision keys off. Truncation here is invisible in the logs and + * would look like a tuning problem, not a sampling one. */ + uint32_t *rates = dogecoin_malloc((size_t)s->num_segs * sizeof(*rates)); + if (!rates) return; + uint32_t n = 0; + for (uint32_t i = 0; i < s->num_segs; i++) { + if (i == s->flush_idx) continue; + uint32_t r = par_hdr_seg_rate(&s->segs[i], now); + if (r > 0) rates[n++] = r; + } + /* Remember the median while a crowd exists, because the tail has none: + * once every other segment has completed their node_id is -1 and + * par_hdr_seg_rate() reports 0 for all of them, so n falls to 0 exactly + * when the head is the only thing left and preemption matters most. + * Fall back to the last crowd-derived median in that case. */ + uint32_t median; + if (n >= 3) { + for (uint32_t i = 1; i < n; i++) { + uint32_t v = rates[i], j = i; + while (j > 0 && rates[j - 1] > v) { rates[j] = rates[j - 1]; j--; } + rates[j] = v; + } + median = rates[n / 2]; + s->rate_ref = median; + } else if (s->rate_ref > 0) { + median = s->rate_ref; + } else { + dogecoin_free(rates); + return; /* never saw a crowd; nothing to call an outlier against */ + } + dogecoin_free(rates); + + if (head_rate * PAR_HDR_SLOW_FACTOR >= median) return; /* not an outlier */ + + /* Only preempt if someone is actually free to pick it up. */ + dogecoin_node *idle = NULL; + if (client->nodegroup && client->nodegroup->nodes) { + for (size_t i = 0; i < client->nodegroup->nodes->len && !idle; i++) { + dogecoin_node *cand = vector_idx(client->nodegroup->nodes, i); + if (!cand) continue; + if (!(cand->state & NODE_CONNECTED) || !cand->version_handshake) continue; + if ((int)cand->nodeid == head->node_id) continue; + dogecoin_bool busy = false; + for (uint32_t k = 0; k < s->num_segs; k++) { + if (s->segs[k].node_id == (int)cand->nodeid) { busy = true; break; } + } + if (!busy) idle = cand; + } + } + if (!idle) return; + + dogecoin_node *owner = par_hdr_node_by_id(client, head->node_id); + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] preempting segment %u from node %d " + "(%u hdr/s vs median %u); %u headers kept, resuming at %u\n", + s->flush_idx, head->node_id, head_rate, median, + head->count, head->tip_height + 1); + + if (owner) owner->state &= ~NODE_HEADERSYNC; + head->node_id = -1; + head->requested_at = 0; + head->assigned_at = 0; + + par_hdr_assign(client, idle); +} + +/* Periodic maintenance for a parallel header sync. + * + * Segments are handed out one per peer and are only cleared on completion, so + * a peer that disconnects or goes silent mid-segment would otherwise hold it + * forever. Because par_hdr_flush only advances across contiguously complete + * segments, a single stuck segment blocks every finished segment above it and + * the sync parks with no getheaders on the wire. + * + * This reclaims those segments, hands free segments to idle peers, and warns + * if the ordered flush stops advancing. + */ +LIBDOGECOIN_API void par_hdr_reclaim(dogecoin_spv_client *client, uint64_t now) +{ + if (!client || !client->par_hdr || !client->par_hdr->active) return; + par_hdr_state *s = client->par_hdr; + + /* 1. Release segments whose owner disconnected or stopped responding. */ + for (uint32_t i = 0; i < s->num_segs; i++) { + par_hdr_seg *seg = &s->segs[i]; + if (seg->complete || seg->node_id == -1) continue; + + dogecoin_node *owner = par_hdr_node_by_id(client, seg->node_id); + dogecoin_bool gone = (owner == NULL) || + !(owner->state & NODE_CONNECTED) || + !owner->version_handshake; + dogecoin_bool stalled = !gone && seg->requested_at > 0 && + now > seg->requested_at && + (now - seg->requested_at) > PAR_HDR_SEG_TIMEOUT; + + if (!gone && !stalled) continue; + + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] releasing segment %u from node %d (%s); " + "%u headers kept, will resume at %u\n", + i, seg->node_id, gone ? "disconnected" : "timed out", + seg->count, seg->tip_height + 1); + + if (owner) owner->state &= ~NODE_HEADERSYNC; + seg->node_id = -1; + seg->requested_at = 0; + seg->shadow_id = -1; + seg->shadow_at = 0; + } + + /* 2. Hand any free segment to a peer that is not already working one. */ + if (client->nodegroup && client->nodegroup->nodes) { + for (size_t i = 0; i < client->nodegroup->nodes->len; i++) { + dogecoin_node *n = vector_idx(client->nodegroup->nodes, i); + if (!n) continue; + if (!(n->state & NODE_CONNECTED) || !n->version_handshake) continue; + par_hdr_assign(client, n); /* no-ops if n already owns a segment */ + } + } + + /* 3. A live but slow owner of the flush head throttles everything above it. */ + par_hdr_preempt_head(client, now); + par_hdr_race_tail(client, now); + + /* 4. Warn if the ordered flush has stopped advancing. */ + if (s->flush_idx != s->last_flush_idx) { + s->last_flush_idx = s->flush_idx; + s->last_progress_time = now; + } else if (s->last_progress_time > 0 && now > s->last_progress_time && + (now - s->last_progress_time) > PAR_HDR_STALL_WARN) { + uint32_t assigned = 0, blocked = 0; + for (uint32_t i = 0; i < s->num_segs; i++) { + if (s->segs[i].node_id != -1) assigned++; + if (s->segs[i].complete && !s->segs[i].flushed) blocked++; + } + if (client->nodegroup && client->nodegroup->log_write_cb) + client->nodegroup->log_write_cb( + "[par-hdr] WARNING: no flush progress for %us — " + "flush_idx=%u/%u, %u segments assigned, %u complete but blocked\n", + (unsigned int)(now - s->last_progress_time), + s->flush_idx, s->num_segs, assigned, blocked); + s->last_progress_time = now; /* rate-limit the warning */ + } +} + +/* Release all par_hdr memory. */ +LIBDOGECOIN_API void par_hdr_free(dogecoin_spv_client *client) +{ + if (!client || !client->par_hdr) return; + par_hdr_state *s = client->par_hdr; + for (uint32_t i = 0; i < s->num_segs; i++) + dogecoin_free(s->segs[i].buf); + dogecoin_free(s->segs); + dogecoin_free(s); + client->par_hdr = NULL; +} + +/* Initialise parallel genesis header download for @client. + * Builds segments from the chainparams block checkpoint array. + * Returns true on success, false if the chain has no checkpoints. */ +LIBDOGECOIN_API dogecoin_bool dogecoin_spv_client_enable_genesis_headers( + dogecoin_spv_client *client) +{ + if (!client || !client->chainparams) return false; + par_hdr_free(client); + client->par_hdr = par_hdr_init(client->chainparams); + if (!client->par_hdr) return false; + return true; +} diff --git a/test/checkpoints_tests.c b/test/checkpoints_tests.c new file mode 100644 index 000000000..d3c3fa708 --- /dev/null +++ b/test/checkpoints_tests.c @@ -0,0 +1,102 @@ +/********************************************************************** + * Copyright (c) 2026 bluezr * + * Copyright (c) 2026 The Dogecoin Foundation * + * Distributed under the MIT software license, see the accompanying * + * file COPYING or http://www.opensource.org/licenses/mit-license.php.* + **********************************************************************/ + +#include + +#include +#include + +#include + +/* The parallel header downloader builds one segment per adjacent pair of + * checkpoints and treats each checkpoint hash as a trust anchor: it skips + * proof-of-work between them and verifies only that each segment lands on its + * terminal hash. Every property that relies on -- segment count, ordering, + * non-overlap, the anchors themselves -- comes from these arrays, and nothing + * checked them. + * + * The exported counts are load-bearing rather than cosmetic. They exist + * because callers used to derive the length with sizeof(array)/sizeof(*array), + * which on an extern array yields whatever bound the header declares. The + * headers said [87] and [33] against 90 real entries, so a checkpoint + * extension compiled, passed the suite, and was silently ignored. */ + +static void check_array(const char *name, + const dogecoin_checkpoint *arr, + size_t count) +{ + size_t i; + + /* A zero count would make every loop below vacuous, which is the failure + * mode this file exists to prevent. */ + u_assert_true(count > 0); + + for (i = 0; i < count; i++) { + size_t hexlen = strlen(arr[i].hash); + + /* 32 bytes, hex-encoded. utils_uint256_sethex reads a fixed width, so + * a short string leaves the tail of the anchor uninitialised rather + * than failing. */ + u_assert_int_eq((int)hexlen, 64); + u_assert_true(strspn(arr[i].hash, "0123456789abcdefABCDEF") == hexlen); + + /* Timestamp and bits are carried into the header DB for the anchor, so + * a zeroed entry would seed it with something meaningless. */ + u_assert_true(arr[i].timestamp != 0); + u_assert_true(arr[i].target != 0); + + if (i == 0) continue; + + /* Strictly increasing. Equal heights would produce a zero-length + * segment; decreasing heights would produce one whose stop is below + * its start, and the downloader would wait forever for headers that + * cannot arrive. */ + u_assert_true(arr[i].height > arr[i - 1].height); + + /* Distinct anchors. Two checkpoints sharing a hash would let a + * segment satisfy its terminal check against the wrong block. */ + u_assert_true(strcmp(arr[i].hash, arr[i - 1].hash) != 0); + } + + (void)name; +} + +void test_checkpoints() +{ + check_array("mainnet", + dogecoin_mainnet_checkpoint_array, + dogecoin_mainnet_checkpoint_count); + + check_array("testnet", + dogecoin_testnet_checkpoint_array, + dogecoin_testnet_checkpoint_count); + + /* Guard the bug that motivated exporting the counts: if anyone reverts to + * sizeof() on the extern array, this stops matching the real length. + * sizeof on an array of unknown bound is a constraint violation, so this + * has to compare against something the definition controls. */ + u_assert_true(dogecoin_mainnet_checkpoint_count > + dogecoin_testnet_checkpoint_count); + + /* Entry 0 is genesis, on both chains. The downloader relies on this: it + * starts building segments at index 1 and supplies genesis separately as + * the first segment's start anchor, so N checkpoints yield N-1 segments. + * If a non-genesis checkpoint were ever prepended, segment 0 would span + * genesis..that height while claiming to span genesis..checkpoint[1], and + * the terminal-hash check would reject it. + * + * Asserted rather than assumed because the author of this test assumed the + * opposite and was wrong. */ + u_assert_int_eq((int)dogecoin_mainnet_checkpoint_array[0].height, 0); + u_assert_int_eq((int)dogecoin_testnet_checkpoint_array[0].height, 0); + + /* Only entry 0 may be genesis; a second height-0 entry would produce a + * zero-length segment. Covered by the strictly-increasing check above, but + * stated here because it is the property the segment builder depends on. */ + u_assert_true(dogecoin_mainnet_checkpoint_array[1].height > 0); + u_assert_true(dogecoin_testnet_checkpoint_array[1].height > 0); +} diff --git a/test/unittester.c b/test/unittester.c index 96dd35fae..64539901e 100644 --- a/test/unittester.c +++ b/test/unittester.c @@ -48,6 +48,7 @@ extern void test_bip44(); extern void test_block_header(); extern void test_buffer(); extern void test_chacha20(); +extern void test_checkpoints(); extern void test_cstr(); extern void test_ecc(); extern void test_hash(); @@ -179,6 +180,7 @@ int main() u_run_test(test_block_header); u_run_test(test_buffer); u_run_test(test_chacha20); + u_run_test(test_checkpoints); u_run_test(test_cstr); u_run_test(test_ecc); u_run_test(test_hash);