diff --git a/.gitignore b/.gitignore index c41cc9e3..2ebc5ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/target \ No newline at end of file +/target +/Cargo.lock \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9aa7d4e5..7aef83b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,23 +218,21 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.13.0", "cc", - "cesu8", - "jni", - "jni-sys", + "jni 0.22.4", "libc", "log", "ndk", "ndk-context", "ndk-sys", "num_enum", - "thiserror 1.0.69", + "thiserror 2.0.18", ] [[package]] @@ -269,9 +267,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -324,7 +322,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "windows-sys 0.52.0", + "windows-sys 0.60.2", "x11rb", ] @@ -354,9 +352,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "as-raw-xcb-connection" @@ -411,9 +409,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -436,7 +434,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -467,7 +465,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -478,14 +476,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -493,7 +491,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -513,7 +511,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -574,15 +572,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -590,21 +588,22 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -720,9 +719,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f3c4f9526d22374fca5b7ff1d6bf8d921ab56db2dac8df66a2c5561b31d4ef" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bip39", @@ -818,7 +817,7 @@ dependencies = [ "error-fatality", "fallible-iterator", "futures", - "hashlink 0.12.0", + "hashlink 0.12.1", "heed 0.22.1", "heed-types", "hex 0.4.3", @@ -831,7 +830,7 @@ dependencies = [ "ordermap", "ouroboros", "parking_lot", - "rand 0.10.1", + "rand 0.10.2", "reqwest 0.13.4", "rusqlite", "rusqlite_migration", @@ -931,7 +930,7 @@ dependencies = [ "base64 0.22.1", "bitcoin", "educe", - "hashlink 0.11.0", + "hashlink 0.11.1", "hex 0.4.3", "http", "jsonrpsee 0.26.0", @@ -985,9 +984,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -1009,16 +1008,16 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", ] [[package]] @@ -1063,25 +1062,35 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", ] [[package]] @@ -1128,28 +1137,28 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1166,9 +1175,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" dependencies = [ "serde", ] @@ -1179,7 +1188,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "log", "polling", "rustix 0.38.44", @@ -1189,13 +1198,13 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "tracing", ] @@ -1218,8 +1227,8 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", - "rustix 1.1.3", + "calloop 0.14.4", + "rustix 1.1.4", "wayland-backend", "wayland-client", ] @@ -1235,14 +1244,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -1274,9 +1283,9 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1285,9 +1294,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1338,7 +1347,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1358,9 +1367,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -1373,23 +1382,23 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width 0.2.2", ] [[package]] name = "color" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18ef4657441fb193b65f34dc39b3781f0dfec23d3bd94d0eeb4e88cde421edb" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" dependencies = [ "bytemuck", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -1480,13 +1489,12 @@ dependencies = [ [[package]] name = "console" -version = "0.16.2" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -1600,18 +1608,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1628,18 +1636,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -1698,7 +1706,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1714,7 +1722,7 @@ dependencies = [ "educe", "either", "futures", - "hashlink 0.11.0", + "hashlink 0.11.1", "imbl", "indexmap", "jsonrpsee 0.26.0", @@ -1733,9 +1741,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -1743,27 +1751,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1778,12 +1785,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "derive_more" @@ -1804,7 +1808,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1848,30 +1852,30 @@ checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2 0.6.4", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ "libloading", ] @@ -1973,7 +1977,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2020,7 +2024,7 @@ checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" dependencies = [ "accesskit", "ahash", - "bitflags 2.10.0", + "bitflags 2.13.0", "emath", "epaint", "log", @@ -2136,22 +2140,22 @@ checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enum-ordinalize" -version = "4.3.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2172,7 +2176,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2266,7 +2270,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2332,7 +2336,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2378,41 +2382,27 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastbloom" -version = "0.14.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" dependencies = [ - "getrandom 0.3.4", + "foldhash 0.2.0", "libm", - "rand 0.9.4", + "portable-atomic", "siphasher", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -2462,9 +2452,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -2525,7 +2515,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2655,7 +2645,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2672,9 +2662,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" dependencies = [ "gloo-timers", "send_wrapper", @@ -2713,7 +2703,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link", ] @@ -2746,16 +2736,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", + "wasm-bindgen", ] [[package]] @@ -2802,9 +2792,9 @@ dependencies = [ [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d" dependencies = [ "futures-channel", "futures-core", @@ -2843,7 +2833,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg_aliases", "cgl", "dispatch2", @@ -2944,7 +2934,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2955,14 +2945,14 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] name = "h2" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -3050,9 +3040,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ "hashbrown 0.16.1", "serde", @@ -3060,9 +3050,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ "hashbrown 0.17.1", "serde", @@ -3095,7 +3085,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "byteorder", "heed-traits", "heed-types", @@ -3114,7 +3104,7 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "byteorder", "heed-traits", "heed-types", @@ -3279,15 +3269,15 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human-size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9994b79e8c1a39b3166c63ae7823bb2b00831e2a96a31399c50fe69df408eaeb" +checksum = "67f1f25c36d3e2a9ed0caf095f1183bb94dd176a1c81521943e416f8ef749fda" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -3300,7 +3290,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3308,20 +3297,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "log", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.8", ] [[package]] @@ -3339,14 +3327,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -3387,12 +3374,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3400,9 +3388,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3413,9 +3401,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3427,15 +3415,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3447,15 +3435,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3466,12 +3454,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -3491,9 +3473,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3501,9 +3483,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -3560,9 +3542,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "futures-core", @@ -3590,19 +3572,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -3630,15 +3602,15 @@ 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" -version = "0.2.28" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" dependencies = [ "jiff-static", "portable-atomic", @@ -3647,13 +3619,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.28" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3665,36 +3637,89 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -3800,8 +3825,8 @@ dependencies = [ "jsonrpsee-types 0.25.1", "parking_lot", "pin-project", - "rand 0.9.4", - "rustc-hash 2.1.1", + "rand 0.9.5", + "rustc-hash 2.1.3", "serde", "serde_json", "thiserror 2.0.18", @@ -3828,8 +3853,8 @@ dependencies = [ "jsonrpsee-types 0.26.0", "parking_lot", "pin-project", - "rand 0.9.4", - "rustc-hash 2.1.1", + "rand 0.9.5", + "rustc-hash 2.1.3", "serde", "serde_json", "thiserror 2.0.18", @@ -3896,7 +3921,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3909,7 +3934,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4098,7 +4123,7 @@ dependencies = [ "proc-macro2", "proc_macro_roids", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4107,12 +4132,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lending-iterator" version = "0.1.7" @@ -4186,13 +4205,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "libc", - "redox_syscall 0.7.0", + "plain", + "redox_syscall 0.9.0", ] [[package]] @@ -4232,15 +4252,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -4279,9 +4299,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "logos" @@ -4289,7 +4309,16 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" dependencies = [ - "logos-derive", + "logos-derive 0.15.1", +] + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive 0.16.1", ] [[package]] @@ -4305,7 +4334,21 @@ dependencies = [ "quote", "regex-syntax", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.118", ] [[package]] @@ -4314,7 +4357,16 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" dependencies = [ - "logos-codegen", + "logos-codegen 0.15.1", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen 0.16.1", ] [[package]] @@ -4372,9 +4424,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -4425,7 +4477,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4445,9 +4497,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniscript" -version = "12.3.5" +version = "12.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c" +checksum = "b8343cc1ef1408bd9bdbf69f7aef47017dfab7e6349ec26fddf62e0e9fb5a4cf" dependencies = [ "bech32", "bitcoin", @@ -4494,14 +4546,14 @@ checksum = "23f5b99488110875b5904839d396c2cdfaf241ff6622638acb879cc7effad5de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -4515,13 +4567,13 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "naga" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -4545,8 +4597,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.13.0", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -4566,7 +4618,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -4663,9 +4715,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -4684,9 +4736,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -4699,11 +4751,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -4732,9 +4783,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -4742,14 +4793,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4783,7 +4834,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4799,7 +4850,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -4812,7 +4863,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4836,7 +4887,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4848,7 +4899,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", ] @@ -4859,7 +4910,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -4902,7 +4953,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "dispatch", "libc", @@ -4915,7 +4966,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -4927,7 +4978,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4950,7 +5001,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4962,7 +5013,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -4974,7 +5025,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4987,7 +5038,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -5010,7 +5061,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -5031,7 +5082,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -5054,7 +5105,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -5063,9 +5114,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -5085,7 +5136,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -5101,7 +5152,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5130,9 +5181,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.50" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" dependencies = [ "libc", "libredox", @@ -5140,9 +5191,9 @@ dependencies = [ [[package]] name = "ordered-float" -version = "4.6.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -5188,7 +5239,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5278,9 +5329,9 @@ dependencies = [ [[package]] name = "peniko" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2b6aadb221872732e87d465213e9be5af2849b0e8cc5300a8ba98fffa2e00a" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" dependencies = [ "bytemuck", "color", @@ -5334,7 +5385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -5357,7 +5408,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5370,7 +5421,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5393,41 +5444,35 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -5446,9 +5491,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "plain_bitassets" @@ -5490,6 +5541,7 @@ dependencies = [ "rayon", "rcgen", "rustls", + "rustreexo", "semver", "serde", "serde_json", @@ -5543,7 +5595,7 @@ dependencies = [ "plain_bitassets_app_rpc_api", "poll-promise", "serde", - "shlex", + "shlex 1.3.0", "strum 0.27.2", "thiserror 2.0.18", "tiny-bip39", @@ -5624,11 +5676,11 @@ dependencies = [ [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -5656,7 +5708,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -5711,18 +5763,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -5755,7 +5807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5808,20 +5860,20 @@ checksum = "d0c2a098cd8aaa29f66da27a684ad19f4b7bc886f576abf12f7df4a7391071c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "profiling" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" [[package]] name = "prost" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -5829,11 +5881,11 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -5844,30 +5896,30 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.117", + "syn 2.0.118", "tempfile", ] [[package]] name = "prost-derive" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "prost-reflect" -version = "0.16.3" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e" +checksum = "01b80ea363c31af2de2b92e3c07ed1156628f7838c4afb4df75ee78a37fedbd1" dependencies = [ - "logos", + "logos 0.16.1", "miette", "prost", "prost-types", @@ -5875,9 +5927,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -5903,7 +5955,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "072eee358134396a4643dff81cfff1c255c9fbd3fb296be14bdb6a26f9156366" dependencies = [ - "logos", + "logos 0.15.1", "miette", "prost-types", "thiserror 2.0.18", @@ -5911,9 +5963,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.184" +version = "2.1.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dc6a90669f481b41cae3005c68efa36bef275b95aa9123a7af7f1c68c6e5b2" +checksum = "f558d33d882cb296fb65dbe0246b98b90e761fbe00c552701d2030eb5bbce63a" dependencies = [ "psl-types", ] @@ -5926,11 +5978,11 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "pulldown-cmark" -version = "0.13.0" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "memchr", "unicase", ] @@ -5946,12 +5998,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "quanta" @@ -5994,7 +6043,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror 2.0.18", @@ -6005,20 +6054,21 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.15" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", "fastbloom", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.4", + "rand 0.10.2", + "rand_pcg", "ring", - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", - "rustls-platform-verifier 0.6.2", + "rustls-platform-verifier 0.7.0", "slab", "thiserror 2.0.18", "tinyvec", @@ -6028,23 +6078,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -6063,9 +6113,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -6074,9 +6124,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6084,12 +6134,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -6137,6 +6187,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -6158,7 +6217,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -6181,9 +6240,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -6237,16 +6296,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] name = "redox_syscall" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -6262,9 +6321,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -6274,9 +6333,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -6285,9 +6344,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "renderdoc-sys" @@ -6330,7 +6389,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.8", ] [[package]] @@ -6366,9 +6425,9 @@ dependencies = [ [[package]] name = "reserve-port" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" +checksum = "71ea98a177596a4579881992bd2bd4af27772fc95d0e5f5668a8f9535eca6380" dependencies = [ "thiserror 2.0.18", ] @@ -6399,7 +6458,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink 0.9.1", @@ -6425,9 +6484,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -6444,31 +6503,31 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "errno 0.3.14", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "log", @@ -6482,9 +6541,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -6494,9 +6553,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -6510,7 +6569,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -6520,18 +6579,18 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -6540,7 +6599,7 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs 1.0.5", + "webpki-root-certs 1.0.8", "windows-sys 0.61.2", ] @@ -6562,11 +6621,19 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustreexo" +version = "0.3.0" +source = "git+https://github.com/mit-dci/rustreexo?rev=6da503b598470f6e87aa9d769b94df64df7ff3ae#6da503b598470f6e87aa9d769b94df64df7ff3ae" +dependencies = [ + "bitcoin_hashes", +] + [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rusty-leveldb" @@ -6579,15 +6646,15 @@ dependencies = [ "errno 0.2.8", "fs2", "integer-encoding", - "rand 0.8.6", + "rand 0.8.7", "snap", ] [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "saa" @@ -6625,9 +6692,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -6659,9 +6726,9 @@ dependencies = [ [[package]] name = "sdd" -version = "4.8.6" +version = "4.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f0e40a01b94e35d1dacbcfbe5bfd3d31e37d9590b2e6d86a82b0e87bd4f551" +checksum = "1836bad8bdc9c6d665b63202da3d9c6d60ed1e597cae63620e21ebf89a3595a9" dependencies = [ "saa", ] @@ -6673,7 +6740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes", - "rand 0.8.6", + "rand 0.8.7", "secp256k1-sys", "serde", ] @@ -6689,11 +6756,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6702,9 +6769,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -6718,9 +6785,9 @@ checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -6728,9 +6795,9 @@ dependencies = [ [[package]] name = "send_wrapper" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" @@ -6759,7 +6826,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6783,7 +6850,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6800,11 +6867,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex 0.4.3", "serde_core", @@ -6815,21 +6883,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -6862,13 +6930,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.2.8", + "errno 0.3.14", "libc", ] @@ -6883,15 +6957,31 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" @@ -6920,9 +7010,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smithay-client-toolkit" @@ -6930,7 +7020,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -6955,14 +7045,14 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", - "calloop 0.14.3", + "bitflags 2.13.0", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", "log", "memmap2", - "rustix 1.1.3", + "rustix 1.1.4", "thiserror 2.0.18", "wayland-backend", "wayland-client", @@ -7068,7 +7158,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.8.6", + "rand 0.8.7", "sha1", ] @@ -7087,7 +7177,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", ] [[package]] @@ -7172,7 +7262,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7184,7 +7274,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7206,9 +7296,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -7241,7 +7331,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7251,14 +7341,14 @@ source = "git+https://gitlab.com/A-Manning/leonhard-llc-ops.git?branch=temp-dir- [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -7297,7 +7387,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7308,23 +7398,23 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", @@ -7336,12 +7426,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -7351,15 +7440,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -7373,7 +7462,7 @@ checksum = "a30fd743a02bf35236f6faf99adb03089bb77e91c998dac2c2ad76bb424f668c" dependencies = [ "once_cell", "pbkdf2", - "rand 0.8.6", + "rand 0.8.7", "rustc-hash 1.1.0", "sha2", "thiserror 1.0.69", @@ -7409,9 +7498,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -7419,9 +7508,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -7457,7 +7546,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7576,14 +7665,14 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tonic-health" -version = "0.14.3" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dbde2c702c4be12b9b2f6f7e6c824a84a7b7be177070cada8ee575a581af359" +checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082" dependencies = [ "prost", "tokio", @@ -7614,7 +7703,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.117", + "syn 2.0.118", "tempfile", "tonic-build", ] @@ -7640,22 +7729,22 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", "http-body-util", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", "tracing", + "url", "uuid", ] @@ -7702,7 +7791,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7777,7 +7866,7 @@ checksum = "baf33f1e1fefdc7c6539d9bd9313296b58618e683606e8df7bd4e3a6758cc6dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7798,14 +7887,14 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 2.1.1", + "rustc-hash 2.1.3", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typewit" @@ -7824,13 +7913,13 @@ checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -7839,7 +7928,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" dependencies = [ - "rand 0.9.4", + "rand 0.9.5", "web-time", ] @@ -7851,9 +7940,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -7866,9 +7955,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -7936,9 +8025,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utoipa" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ "indexmap", "serde", @@ -7948,22 +8037,22 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "5.4.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -8061,27 +8150,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -8092,23 +8172,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8116,69 +8192,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.10.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "wayland-backend" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.3", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -8186,12 +8228,12 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.12" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.10.0", - "rustix 1.1.3", + "bitflags 2.13.0", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -8202,29 +8244,29 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.12" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5864c4b5b6064b06b1e8b74ead4a98a6c45a285fe7a0e784d24735f011fdb078" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.32.10" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8236,7 +8278,7 @@ version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8245,11 +8287,11 @@ dependencies = [ [[package]] name = "wayland-protocols-misc" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8258,11 +8300,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8271,11 +8313,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.10" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8295,9 +8337,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -8307,9 +8349,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -8327,12 +8369,12 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.6" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" dependencies = [ "core-foundation 0.10.1", - "jni", + "jni 0.22.4", "log", "ndk-context", "objc2 0.6.4", @@ -8347,14 +8389,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.5", + "webpki-root-certs 1.0.8", ] [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -8367,9 +8409,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -8382,12 +8424,12 @@ checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "wgpu" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "cfg-if", "cfg_aliases", @@ -8412,14 +8454,14 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "document-features", @@ -8446,51 +8488,51 @@ dependencies = [ [[package]] name = "wgpu-core-deps-apple" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e51b5447e144b3dbba4feb01f80f4fa21696fa0cd99afb2c3df1affd6fdb28" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3487cd6293a963bc5c0c0396f6a2192043c50003c07f4efdccbad3d90ec9d819" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-wasm" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2f2fb042f36920771deb0b966543c5751b18f3d327760ffc90f74e20b2dcd4" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.6.2", "bytemuck", "cfg-if", @@ -8536,9 +8578,9 @@ dependencies = [ [[package]] name = "wgpu-naga-bridge" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" dependencies = [ "naga", "wgpu-types", @@ -8546,11 +8588,11 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.3" +version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -8601,7 +8643,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8663,7 +8705,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8674,7 +8716,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -8729,6 +8771,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -8762,13 +8822,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -8790,6 +8867,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8802,6 +8885,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8814,12 +8903,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8832,6 +8933,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8844,6 +8951,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8856,6 +8969,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8868,6 +8987,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.13" @@ -8877,7 +9002,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.13.0", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -8931,97 +9056,15 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x11-dl" @@ -9045,7 +9088,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -9079,7 +9122,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.0", "dlib", "log", "once_cell", @@ -9109,9 +9152,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -9120,21 +9163,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zbus" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" dependencies = [ "async-broadcast", "async-executor", @@ -9152,7 +9195,7 @@ dependencies = [ "hex 0.4.3", "libc", "ordered-stream", - "rustix 1.1.3", + "rustix 1.1.4", "serde", "serde_repr", "tracing", @@ -9183,7 +9226,7 @@ checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus-lockstep", "zbus_xml", "zvariant", @@ -9191,14 +9234,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus_names", "zvariant", "zvariant_utils", @@ -9229,63 +9272,63 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -9303,7 +9346,7 @@ dependencies = [ "num-traits", "once_cell", "parking_lot", - "rand 0.9.4", + "rand 0.9.5", "regex", "scc", "thiserror 1.0.69", @@ -9315,9 +9358,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -9326,9 +9369,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -9337,26 +9380,26 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zlib-rs" -version = "0.5.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" @@ -9388,15 +9431,15 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.4.12" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] @@ -9424,7 +9467,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils", ] @@ -9437,6 +9480,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", + "syn 2.0.118", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index d0ff0d2b..27960efd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,10 @@ rev = "1100b6085d9ed64eeaf1b57b3c44609626bd4b23" git = "https://github.com/Ash-L2L/l2l-openapi" rev = "5f8fb2237c95725731d5fb10542098303d82e215" +[workspace.dependencies.rustreexo] +git = "https://github.com/mit-dci/rustreexo" +rev = "6da503b598470f6e87aa9d769b94df64df7ff3ae" + [workspace.lints.clippy] all = { level = "deny", priority = -1 } assertions_on_result_states = "allow" diff --git a/app/app.rs b/app/app.rs index 5b4b6998..77113188 100644 --- a/app/app.rs +++ b/app/app.rs @@ -61,7 +61,7 @@ fn update_wallet(node: &Node, wallet: &Wallet) -> Result<(), Error> { let addresses = wallet.get_addresses()?; let unconfirmed_utxos = node.get_unconfirmed_utxos_by_addresses(&addresses)?; - let utxos = node.get_utxos_by_addresses(&addresses)?; + let utxos = node.get_utxos_by_addresses(&addresses, 0)?; let confirmed_outpoints: Vec<_> = wallet.get_utxos()?.into_keys().collect(); let confirmed_spent = node .get_spent_utxos(&confirmed_outpoints)? diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 0c5624d7..9f4583a1 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -45,6 +45,7 @@ prost-types = { workspace = true } quinn = { workspace = true } rayon = { workspace = true } rcgen = { workspace = true } +rustreexo = { workspace = true } rustls = { workspace = true, features = ["ring"] } semver = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } diff --git a/lib/archive.rs b/lib/archive.rs index 181563e1..c7ccfccf 100644 --- a/lib/archive.rs +++ b/lib/archive.rs @@ -6,7 +6,8 @@ use std::{ use bitcoin::{self, hashes::Hash as _}; use fallible_iterator::{FallibleIterator, IteratorExt}; -use heed::types::SerdeBincode; +use heed::types::{Bytes, SerdeBincode}; +use serde::{Deserialize, Serialize}; use sneed::{ DatabaseUnique, EnvError, RoTxn, RwTxn, RwTxnError, UnitKey, db::{self, error::Error as DbError}, @@ -14,10 +15,22 @@ use sneed::{ }; use crate::types::{ - Block, BlockHash, BmmResult, Body, Header, Tip, Txid, VERSION, Version, - proto::mainchain, + Accumulator, AccumulatorDiff, Address, AddressTxidKey, Block, BlockHash, + BmmResult, Body, FilledTransaction, Header, MerkleProof, Tip, Txid, + VERSION, Version, proto::mainchain, }; +pub const TXDB_PRUNE_INTERVAL_BLOCKS: u32 = 144 * 7; +pub const TXDB_RETENTION_SECS: u64 = 28 * 24 * 60 * 60; + +/// Check for accumulator history eligible for pruning once per week. +pub const ACCUMULATOR_PRUNE_INTERVAL_BLOCKS: u32 = 144 * 7; +/// Maximum number of sidechain blocks that may be disconnected in one reorg. +/// Snapshots and diffs required to reconstruct this window are retained. +pub const ACCUMULATOR_REORG_HORIZON_BLOCKS: u32 = 144 * 7 * 4 * 6; +/// Persist one full accumulator snapshot per week. +pub const ACCUMULATOR_SNAPSHOT_INTERVAL_BLOCKS: u32 = 144 * 7; + #[allow(clippy::duplicated_attributes)] #[derive(Debug, thiserror::Error, transitive::Transitive)] #[transitive( @@ -76,10 +89,31 @@ pub enum Error { NoMainHeight(bitcoin::BlockHash), #[error("no tx with txid {0}")] NoTx(Txid), + #[error("no accumulator diff for block {0}")] + NoAccumulatorDiff(BlockHash), + #[error(transparent)] + Utreexo(#[from] crate::types::UtreexoError), + #[error( + "txdb filled transaction length mismatch: expected {expected}, actual {actual}" + )] + TxDbFilledTxLenMismatch { expected: usize, actual: usize }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct FilledTxEntry { + pub tx: FilledTransaction, + pub merkle_proof: MerkleProof, + pub block_hash: BlockHash, + pub block_height: u32, + pub unix_stamp: u64, } #[derive(Clone)] pub struct Archive { + accumulators: DatabaseUnique, Bytes>, + accumulator_diffs: + DatabaseUnique, SerdeBincode>, + accumulator_reorg_floor: DatabaseUnique>, block_hash_to_height: DatabaseUnique, SerdeBincode>, /// BMM results for each header. @@ -151,11 +185,13 @@ pub struct Archive { SerdeBincode, SerdeBincode>, >, + /// Capped tx cache, keyed by (address, txid). + txdb: DatabaseUnique>, _version: DatabaseUnique>, } impl Archive { - pub const NUM_DBS: u32 = 14; + pub const NUM_DBS: u32 = 15 + 3; pub fn new(env: &sneed::Env) -> Result { let mut rwtxn = env.write_txn()?; @@ -180,6 +216,13 @@ impl Archive { Some(_) => (), None => version.put(&mut rwtxn, &(), &*VERSION)?, } + let accumulators = + DatabaseUnique::create(env, &mut rwtxn, "accumulators") + .map_err(EnvError::from)?; + let accumulator_diffs = + DatabaseUnique::create(env, &mut rwtxn, "accumulator_diffs")?; + let accumulator_reorg_floor = + DatabaseUnique::create(env, &mut rwtxn, "accumulator_reorg_floor")?; let block_hash_to_height = DatabaseUnique::create(env, &mut rwtxn, "hash_to_height")?; let bmm_results = @@ -227,8 +270,12 @@ impl Archive { let total_work = DatabaseUnique::create(env, &mut rwtxn, "total_work")?; let txid_to_inclusions = DatabaseUnique::create(env, &mut rwtxn, "txid_to_inclusions")?; + let txdb = DatabaseUnique::create(env, &mut rwtxn, "txdb")?; rwtxn.commit()?; Ok(Self { + accumulators, + accumulator_diffs, + accumulator_reorg_floor, block_hash_to_height, bmm_results, bodies, @@ -242,6 +289,7 @@ impl Archive { successors, total_work, txid_to_inclusions, + txdb, _version: version, }) } @@ -453,6 +501,67 @@ impl Archive { .max_by_key(|main_hash| self.get_total_work(rotxn, *main_hash)) } + fn best_main_verification_on_lineage( + &self, + rotxn: &RoTxn, + verifications: &HashMap, + main_tip: bitcoin::BlockHash, + ) -> Result, Error> { + if main_tip == bitcoin::BlockHash::all_zeros() { + return Ok(None); + } + let mut best = None; + for (main_block, result) in verifications { + if *result != BmmResult::Verified + || !self.is_main_descendant(rotxn, *main_block, main_tip)? + { + continue; + } + let work = self.get_total_work(rotxn, *main_block)?; + if best.is_none_or(|(_, best_work)| work > best_work) { + best = Some((*main_block, work)); + } + } + Ok(best.map(|(main_block, _)| main_block)) + } + + /// Return the newest valid BMM verification for `block_hash` that is on + /// the ancestry of `main_tip`. + pub fn try_get_best_main_verification_on_lineage( + &self, + rotxn: &RoTxn, + block_hash: BlockHash, + main_tip: bitcoin::BlockHash, + ) -> Result, Error> { + let verifications = self.get_bmm_results(rotxn, block_hash)?; + self.best_main_verification_on_lineage(rotxn, &verifications, main_tip) + } + + /// Return every archived sidechain block with a valid BMM verification on + /// the specified canonical mainchain lineage. + pub fn get_mainchain_verified_tips( + &self, + rotxn: &RoTxn, + main_tip: bitcoin::BlockHash, + ) -> Result, Error> { + let mut tips = Vec::new(); + let mut verifications = + self.bmm_results.iter(rotxn).map_err(DbError::from)?; + while let Some((block_hash, results)) = + verifications.next().map_err(DbError::from)? + { + if let Some(main_block_hash) = self + .best_main_verification_on_lineage(rotxn, &results, main_tip)? + { + tips.push(Tip { + block_hash, + main_block_hash, + }); + } + } + Ok(tips) + } + /// Try to get the best valid mainchain verification for the specified block. pub fn get_best_main_verification( &self, @@ -603,6 +712,207 @@ impl Archive { Ok(res) } + /// Reconstruct the accumulator at `target` from the newest snapshot on its + /// ancestry and the subsequent per-block diffs. + /// + /// Diffs are deliberately applied one block at a time because combining + /// changes across block boundaries loses accumulator insertion history. + pub fn accumulator_at( + &self, + rotxn: &RoTxn, + target: Option, + ) -> Result { + let Some(target) = target else { + return Ok(Accumulator::default()); + }; + let target_height = self.get_height(rotxn, target)?; + let mut newest_snapshot: Option<(u32, BlockHash)> = None; + let mut snapshots = + self.accumulators.iter(rotxn).map_err(DbError::from)?; + while let Some((block_hash, snapshot_bytes)) = + snapshots.next().map_err(DbError::from)? + { + let snapshot_height = + Self::decode_accumulator_snapshot_height(snapshot_bytes)?; + if snapshot_height > target_height + || newest_snapshot + .as_ref() + .is_some_and(|(height, _)| *height >= snapshot_height) + { + continue; + } + if self.is_descendant(rotxn, block_hash, target)? { + newest_snapshot = Some((snapshot_height, block_hash)); + } + } + drop(snapshots); + + let (snapshot_height, mut accumulator) = + if let Some((height, block_hash)) = newest_snapshot { + let snapshot_bytes = self + .accumulators + .get(rotxn, &block_hash) + .map_err(DbError::from)?; + let accumulator = + Self::decode_accumulator_snapshot(snapshot_bytes)?; + (Some(height), accumulator) + } else { + (None, Accumulator::default()) + }; + let blocks = + self.ancestor_hashes_after_height(rotxn, target, snapshot_height)?; + for block_hash in blocks { + accumulator + .apply_diff(self.get_accumulator_diff(rotxn, block_hash)?)?; + } + Ok(accumulator) + } + + /// Store an accumulator snapshot for a specific block. + pub fn put_accumulator( + &self, + rwtxn: &mut RwTxn, + block_hash: BlockHash, + block_height: u32, + accumulator: &Accumulator, + ) -> Result<(), Error> { + let mut snapshot = Vec::new(); + snapshot.extend_from_slice(&block_height.to_le_bytes()); + accumulator.serialize_into(&mut snapshot)?; + self.accumulators + .put(rwtxn, &block_hash, &snapshot) + .map_err(DbError::from)?; + Ok(()) + } + + fn decode_accumulator_snapshot(bytes: &[u8]) -> Result { + Self::decode_accumulator_snapshot_height(bytes)?; + let accumulator_bytes = &bytes[4..]; + Accumulator::deserialize_from(accumulator_bytes).map_err(Into::into) + } + + fn decode_accumulator_snapshot_height(bytes: &[u8]) -> Result { + let height_bytes = bytes.first_chunk::<4>().ok_or_else(|| { + crate::types::UtreexoError::custom( + "accumulator snapshot is shorter than its height prefix", + ) + })?; + Ok(u32::from_le_bytes(*height_bytes)) + } + + /// Oldest sidechain height for which retained accumulator history is + /// guaranteed to support reconstruction. This value only moves forward, + /// even if the active sidechain later reorganizes to a lower height. + pub fn accumulator_reorg_floor(&self, rotxn: &RoTxn) -> Result { + self.accumulator_reorg_floor + .try_get(rotxn, &()) + .map(|floor| floor.unwrap_or_default()) + .map_err(|err| DbError::from(err).into()) + } + + pub fn advance_accumulator_reorg_floor( + &self, + rwtxn: &mut RwTxn, + new_floor: u32, + ) -> Result<(), Error> { + let current_floor = self.accumulator_reorg_floor(rwtxn)?; + if new_floor > current_floor { + self.accumulator_reorg_floor + .put(rwtxn, &(), &new_floor) + .map_err(DbError::from)?; + } + Ok(()) + } + + pub fn prune_accumulator_older_than( + &self, + rwtxn: &mut RwTxn, + threshold_block_height: u32, + ) -> Result<(), Error> { + let keys = { + let mut keys = Vec::new(); + let mut iter = + self.accumulators.iter(rwtxn).map_err(DbError::from)?; + while let Some((block_hash, snapshot_bytes)) = + iter.next().map_err(DbError::from)? + { + let snapshot_height = + Self::decode_accumulator_snapshot_height(snapshot_bytes)?; + if snapshot_height < threshold_block_height { + keys.push(block_hash); + } + } + keys + }; + for key in &keys { + let _ = self.accumulators.delete(rwtxn, key)?; + } + Ok(()) + } + + /// AccumulatorDiff + pub fn put_accumulator_diff( + &self, + rwtxn: &mut RwTxn, + block_hash: BlockHash, + diff: &AccumulatorDiff, + ) -> Result<(), Error> { + self.accumulator_diffs + .put(rwtxn, &block_hash, diff) + .map_err(DbError::from)?; + Ok(()) + } + + pub fn try_get_accumulator_diff( + &self, + rotxn: &RoTxn, + block_hash: BlockHash, + ) -> Result, Error> { + let diff = self + .accumulator_diffs + .try_get(rotxn, &block_hash) + .map_err(DbError::from)?; + Ok(diff) + } + + pub fn get_accumulator_diff( + &self, + rotxn: &RoTxn, + block_hash: BlockHash, + ) -> Result { + self.try_get_accumulator_diff(rotxn, block_hash)? + .ok_or(Error::NoAccumulatorDiff(block_hash)) + } + + pub fn prune_accumulator_diffs_older_than( + &self, + rwtxn: &mut RwTxn, + threshold_block_height: u32, + ) -> Result<(), Error> { + let keys = { + let mut keys = Vec::new(); + let mut iter = + self.accumulator_diffs.iter(rwtxn).map_err(DbError::from)?; + + while let Some((block_hash, _diff)) = + iter.next().map_err(DbError::from)? + { + let height = self.get_height(rwtxn, block_hash)?; + if height < threshold_block_height { + keys.push(block_hash); + } + } + + keys + }; + + for key in &keys { + let _ = self.accumulator_diffs.delete(rwtxn, key)?; + } + + Ok(()) + } + pub fn try_get_main_successors( &self, rotxn: &RoTxn, @@ -675,6 +985,125 @@ impl Archive { Ok(inclusions) } + pub fn prune_txdb_block( + &self, + rwtxn: &mut RwTxn, + block_hash: BlockHash, + ) -> Result<(), Error> { + let keys = { + let mut keys = Vec::new(); + let mut iter = self.txdb.iter(rwtxn).map_err(DbError::from)?; + while let Some((key, entry)) = iter.next().map_err(DbError::from)? { + if entry.block_hash == block_hash { + keys.push(key); + } + } + keys + }; + for key in &keys { + let _ = self.txdb.delete(rwtxn, key)?; + } + Ok(()) + } + + pub fn prune_txdb_older_than( + &self, + rwtxn: &mut RwTxn, + cutoff_unix: u64, + ) -> Result<(), Error> { + let keys = { + let mut keys = Vec::new(); + let mut iter = self.txdb.iter(rwtxn).map_err(DbError::from)?; + while let Some((key, entry)) = iter.next().map_err(DbError::from)? { + if entry.unix_stamp < cutoff_unix { + keys.push(key); + } + } + keys + }; + for key in &keys { + let _ = self.txdb.delete(rwtxn, key)?; + } + Ok(()) + } + + pub fn put_txdb_for_connected_block( + &self, + rwtxn: &mut RwTxn, + block_hash: BlockHash, + block_height: u32, + body: &Body, + filled_txs: &[FilledTransaction], + unix_stamp: u64, + ) -> Result<(), Error> { + if body.transactions.len() != filled_txs.len() { + return Err(Error::TxDbFilledTxLenMismatch { + expected: body.transactions.len(), + actual: filled_txs.len(), + }); + } + for (tx_index, filled_tx) in filled_txs.iter().enumerate() { + let txid = filled_tx.txid(); + debug_assert_eq!(body.transactions[tx_index].txid(), txid); + let merkle_proof = Body::compute_tx_merkle_proof( + &body.coinbase, + &body.transactions, + tx_index, + ); + debug_assert_eq!(merkle_proof.leaf_index, tx_index + 1); + + let mut addresses = HashSet::new(); + addresses.extend( + filled_tx.spent_utxos.iter().map(|output| output.address), + ); + addresses.extend( + filled_tx + .transaction + .outputs + .iter() + .map(|output| output.address), + ); + + for address in addresses { + let key = AddressTxidKey::new(address, txid); + let entry = FilledTxEntry { + tx: filled_tx.clone(), + merkle_proof: merkle_proof.clone(), + block_hash, + block_height, + unix_stamp, + }; + self.txdb.put(rwtxn, &key, &entry)?; + } + } + Ok(()) + } + + pub fn get_txdb_entries_by_address( + &self, + rotxn: &RoTxn, + addresses: &HashSet
, + height_threshold: u32, + ) -> Result, Error> { + let mut entries = Vec::new(); + for address in addresses { + let start = AddressTxidKey::start(*address); + let end = AddressTxidKey::end(*address); + let mut iter = self + .txdb + .range(rotxn, &(start..=end)) + .map_err(DbError::from)?; + while let Some((_key, entry)) = + iter.next().map_err(DbError::from)? + { + if entry.block_height >= height_threshold { + entries.push(entry); + } + } + } + Ok(entries) + } + /// Store a block body. The header must already exist. pub fn put_body( &self, @@ -1018,6 +1447,33 @@ impl Archive { } } + /// Return the ancestry after `exclusive_height`, ordered oldest-to-newest. + /// + /// If `exclusive_height` is `None`, return the full ancestry, including the + /// genesis block at height zero. + pub fn ancestor_hashes_after_height( + &self, + rotxn: &RoTxn, + tip_hash: BlockHash, + exclusive_height: Option, + ) -> Result, Error> { + let mut blocks = Vec::new(); + let mut ancestors = self.ancestors(rotxn, tip_hash); + + while let Some(block_hash) = ancestors.next()? { + let block_height = self.get_height(rotxn, block_hash)?; + + if exclusive_height.is_some_and(|height| block_height <= height) { + break; + } + + blocks.push(block_hash); + } + + blocks.reverse(); + Ok(blocks) + } + /// Get missing bodies in the ancestry of the specified block, up to the /// specified ancestor. /// The specified ancestor must exist. @@ -1598,3 +2054,239 @@ impl FallibleIterator for AncestorsRev<'_, '_> { } } } + +#[cfg(test)] +mod tests { + use bitcoin::hashes::Hash as _; + + use super::*; + + fn temp_env( + test_name: &str, + ) -> anyhow::Result<(temp_dir::TempDir, sneed::Env)> { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos(); + let temp_dir = temp_dir::TempDir::with_prefix(format!( + "plain-bitassets-{test_name}-{}-{nanos}", + std::process::id() + ))?; + let mut opts = heed::EnvOpenOptions::new(); + opts.map_size(64 * 1024 * 1024).max_dbs(Archive::NUM_DBS); + let env = unsafe { sneed::Env::open(&opts, temp_dir.path()) }?; + Ok((temp_dir, env)) + } + + fn clone_accumulator( + accumulator: &Accumulator, + ) -> Result { + let mut bytes = Vec::new(); + accumulator.serialize_into(&mut bytes)?; + Accumulator::deserialize_from(&bytes) + } + + fn header(prev_side_hash: Option, body: &Body) -> Header { + Header { + merkle_root: Body::compute_merkle_root( + &body.coinbase, + &body.transactions, + ), + prev_side_hash, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + } + } + + fn marked_header(prev_side_hash: Option, marker: u8) -> Header { + Header { + merkle_root: [marker; 32].into(), + prev_side_hash, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + } + } + + fn apply_insert( + accumulator: &mut Accumulator, + hash: [u8; 32], + ) -> anyhow::Result<()> { + let mut diff = AccumulatorDiff::default(); + diff.insert(hash); + accumulator + .apply_diff(diff) + .map_err(|err| anyhow::anyhow!(err)) + } + + #[test] + fn accumulator_reconstruction_ignores_other_branch_snapshots() + -> anyhow::Result<()> { + let (_temp_dir, env) = temp_env( + "accumulator_reconstruction_ignores_other_branch_snapshots", + )?; + let archive = Archive::new(&env)?; + + let genesis = marked_header(None, 1); + let genesis_hash = genesis.hash(); + let a1 = marked_header(Some(genesis_hash), 2); + let a1_hash = a1.hash(); + let b1 = marked_header(Some(genesis_hash), 3); + let b1_hash = b1.hash(); + let b2 = marked_header(Some(b1_hash), 4); + let b2_hash = b2.hash(); + + let mut genesis_accumulator = Accumulator::default(); + apply_insert(&mut genesis_accumulator, [10; 32])?; + + let mut b1_diff = AccumulatorDiff::default(); + b1_diff.insert([30; 32]); + let mut b2_diff = AccumulatorDiff::default(); + b2_diff.insert([40; 32]); + + let mut rwtxn = env.write_txn()?; + archive.put_header(&mut rwtxn, &genesis)?; + archive.put_header(&mut rwtxn, &a1)?; + archive.put_header(&mut rwtxn, &b1)?; + archive.put_header(&mut rwtxn, &b2)?; + archive.put_accumulator( + &mut rwtxn, + genesis_hash, + 0, + &genesis_accumulator, + )?; + // A non-ancestral snapshot's forest is never deserialized. + let mut malformed_abandoned_snapshot = 1_u32.to_le_bytes().to_vec(); + malformed_abandoned_snapshot.push(0xff); + archive.accumulators.put( + &mut rwtxn, + &a1_hash, + &malformed_abandoned_snapshot, + )?; + archive.put_accumulator_diff(&mut rwtxn, b1_hash, &b1_diff)?; + archive.put_accumulator_diff(&mut rwtxn, b2_hash, &b2_diff)?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + let actual = archive.accumulator_at(&rotxn, Some(b2_hash))?; + let mut expected = clone_accumulator(&genesis_accumulator)?; + expected.apply_diff(b1_diff)?; + expected.apply_diff(b2_diff)?; + assert_eq!( + bincode::serialize(&actual)?, + bincode::serialize(&expected)? + ); + assert_eq!( + bincode::serialize(&archive.accumulator_at(&rotxn, None)?)?, + bincode::serialize(&Accumulator::default())? + ); + Ok(()) + } + + #[test] + fn accumulator_pruning_retains_reconstruction_anchor() -> anyhow::Result<()> + { + let (_temp_dir, env) = + temp_env("accumulator_pruning_retains_reconstruction_anchor")?; + let archive = Archive::new(&env)?; + let block0 = marked_header(None, 10); + let hash0 = block0.hash(); + let block1 = marked_header(Some(hash0), 11); + let hash1 = block1.hash(); + let block2 = marked_header(Some(hash1), 12); + let hash2 = block2.hash(); + + let mut diff0 = AccumulatorDiff::default(); + diff0.insert([10; 32]); + let mut diff1 = AccumulatorDiff::default(); + diff1.insert([11; 32]); + let mut diff2 = AccumulatorDiff::default(); + diff2.insert([12; 32]); + let mut accumulator0 = Accumulator::default(); + accumulator0.apply_diff(diff0.clone())?; + let mut accumulator1 = clone_accumulator(&accumulator0)?; + accumulator1.apply_diff(diff1.clone())?; + let mut expected = clone_accumulator(&accumulator1)?; + expected.apply_diff(diff2.clone())?; + + let mut rwtxn = env.write_txn()?; + for header in [&block0, &block1, &block2] { + archive.put_header(&mut rwtxn, header)?; + } + archive.put_accumulator(&mut rwtxn, hash0, 0, &accumulator0)?; + archive.put_accumulator(&mut rwtxn, hash1, 1, &accumulator1)?; + archive.put_accumulator_diff(&mut rwtxn, hash0, &diff0)?; + archive.put_accumulator_diff(&mut rwtxn, hash1, &diff1)?; + archive.put_accumulator_diff(&mut rwtxn, hash2, &diff2)?; + archive.prune_accumulator_older_than(&mut rwtxn, 1)?; + archive.prune_accumulator_diffs_older_than(&mut rwtxn, 1)?; + archive.advance_accumulator_reorg_floor(&mut rwtxn, 1)?; + archive.advance_accumulator_reorg_floor(&mut rwtxn, 0)?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + let actual = archive.accumulator_at(&rotxn, Some(hash2))?; + assert_eq!( + bincode::serialize(&actual)?, + bincode::serialize(&expected)? + ); + assert!(archive.try_get_accumulator_diff(&rotxn, hash0)?.is_none()); + assert_eq!(archive.accumulator_reorg_floor(&rotxn)?, 1); + Ok(()) + } + + #[test] + fn ancestor_hashes_without_height_include_genesis() -> anyhow::Result<()> { + let (_temp_dir, env) = + temp_env("ancestor_hashes_without_height_include_genesis")?; + let archive = Archive::new(&env)?; + let body = Body { + coinbase: Vec::new(), + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let genesis = header(None, &body); + let genesis_hash = genesis.hash(); + let child = header(Some(genesis_hash), &body); + let child_hash = child.hash(); + let grandchild = header(Some(child_hash), &body); + let grandchild_hash = grandchild.hash(); + + let mut rwtxn = env.write_txn()?; + archive.put_header(&mut rwtxn, &genesis)?; + archive.put_header(&mut rwtxn, &child)?; + archive.put_header(&mut rwtxn, &grandchild)?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + assert_eq!( + archive.ancestor_hashes_after_height( + &rotxn, + grandchild_hash, + None, + )?, + vec![genesis_hash, child_hash, grandchild_hash] + ); + assert_eq!( + archive.ancestor_hashes_after_height( + &rotxn, + grandchild_hash, + Some(0), + )?, + vec![child_hash, grandchild_hash] + ); + assert_eq!( + archive.ancestor_hashes_after_height( + &rotxn, + grandchild_hash, + Some(1), + )?, + vec![grandchild_hash] + ); + assert_eq!( + archive.ancestor_hashes_after_height( + &rotxn, + grandchild_hash, + Some(2), + )?, + Vec::new() + ); + Ok(()) + } +} diff --git a/lib/authorization.rs b/lib/authorization.rs index 2d9883dc..3d85a151 100644 --- a/lib/authorization.rs +++ b/lib/authorization.rs @@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::types::{ - Address, AuthorizedTransaction, Body, GetAddress, Transaction, Verify, - VerifyingKey, + ADDRESS_SIZE, Address, AuthorizedTransaction, Body, GetAddress, + Transaction, Verify, VerifyingKey, }; pub use ed25519_dalek::{SignatureError, Signer, SigningKey, Verifier}; @@ -151,7 +151,7 @@ impl Verify for Authorization { pub fn get_address(verifying_key: &VerifyingKey) -> Address { let mut hasher = blake3::Hasher::new(); let mut reader = hasher.update(&verifying_key.to_bytes()).finalize_xof(); - let mut output: [u8; 20] = [0; 20]; + let mut output: [u8; ADDRESS_SIZE] = [0; ADDRESS_SIZE]; reader.fill(&mut output); Address(output) } diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 771ef618..a7260a45 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -16,7 +16,9 @@ use tracing::instrument; use crate::{ archive::Archive, state::State, - types::{AuthorizedTransaction, Network, THIS_SIDECHAIN, VERSION, Version}, + types::{ + AuthorizedTransaction, M6id, Network, THIS_SIDECHAIN, VERSION, Version, + }, }; pub mod error; @@ -186,6 +188,7 @@ pub struct Net { archive: Archive, network: Network, state: State, + canonical_main_tip: Arc>>, active_peers: Arc>>, // None indicates that the stream has ended peer_info_tx: @@ -225,6 +228,14 @@ impl Net { } } + pub(crate) fn canonical_main_tip(&self) -> Option { + *self.canonical_main_tip.read() + } + + pub(crate) fn set_canonical_main_tip(&self, main_tip: bitcoin::BlockHash) { + *self.canonical_main_tip.write() = Some(main_tip); + } + /// Apply the provided function to the peer connection handle, /// if it exists. pub fn try_with_active_peer_connection( @@ -278,6 +289,7 @@ impl Net { let connection_ctxt = PeerConnectionCtxt { env, archive: self.archive.clone(), + canonical_main_tip: Arc::clone(&self.canonical_main_tip), network: self.network, state: self.state.clone(), }; @@ -320,6 +332,7 @@ impl Net { ) -> Result<(Self, PeerInfoRx), Error> { let (server, _) = make_server_endpoint(bind_addr)?; let active_peers = Arc::new(RwLock::new(HashMap::new())); + let canonical_main_tip = Arc::new(RwLock::new(None)); let mut rwtxn = env.write_txn()?; let known_peers = match DatabaseUnique::open(env, &rwtxn, "known_peers")? { @@ -344,6 +357,7 @@ impl Net { archive, network, state, + canonical_main_tip, active_peers, peer_info_tx, known_peers, @@ -441,6 +455,7 @@ impl Net { let connection_ctxt = PeerConnectionCtxt { env, archive: self.archive.clone(), + canonical_main_tip: Arc::clone(&self.canonical_main_tip), network: self.network, state: self.state.clone(), }; @@ -518,4 +533,47 @@ impl Net { } }) } + + /// Request durable withdrawal bundle metadata from connected peers. + /// Returns the number of connected peer tasks to which it was queued. + pub fn request_withdrawal_bundle( + &self, + m6id: M6id, + preferred: Option, + ) -> usize { + let active_peers = self.active_peers.read(); + let mut requested = 0; + let mut request_peer = + |addr: &SocketAddr, + peer_connection_handle: &PeerConnectionHandle| { + if peer_connection_handle.connection_status() + != PeerConnectionStatus::Connected + { + return; + } + let request: PeerRequest = + peer::message::GetWithdrawalBundleRequest { m6id }.into(); + if peer_connection_handle + .internal_message_tx + .unbounded_send(request.into()) + .is_ok() + { + requested += 1; + } else { + tracing::warn!(%addr, %m6id, "failed to request withdrawal bundle metadata"); + } + }; + + if let Some(preferred) = preferred + && let Some(peer) = active_peers.get(&preferred) + { + request_peer(&preferred, peer); + } + for (addr, peer) in active_peers.iter() { + if Some(*addr) != preferred { + request_peer(addr, peer); + } + } + requested + } } diff --git a/lib/net/peer/message.rs b/lib/net/peer/message.rs index 9e87a977..f753c2f7 100644 --- a/lib/net/peer/message.rs +++ b/lib/net/peer/message.rs @@ -2,13 +2,15 @@ use std::{collections::HashSet, num::NonZeroUsize}; +use bitcoin::hashes::Hash as _; use borsh::BorshSerialize; use serde::{Deserialize, Serialize}; use crate::{ net::peer::{PeerState, PeerStateId}, types::{ - AuthorizedTransaction, BlockHash, Body, Header, Network, Tip, Txid, + AuthorizedTransaction, BlockHash, Body, Header, M6id, Network, Tip, + Txid, WithdrawalBundleMetadata, }, }; @@ -62,6 +64,30 @@ impl GetBlockRequest { } } +fn borsh_serialize_m6id(m6id: &M6id, writer: &mut W) -> borsh::io::Result<()> +where + W: borsh::io::Write, +{ + borsh::BorshSerialize::serialize(&m6id.0.to_byte_array(), writer) +} + +#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize)] +pub struct GetWithdrawalBundleRequest { + #[borsh(serialize_with = "borsh_serialize_m6id")] + pub m6id: M6id, +} + +impl GetWithdrawalBundleRequest { + pub const fn read_response_limit(&self) -> NonZeroUsize { + const RESPONSE_ENVELOPE_HEADROOM: usize = 1024; + NonZeroUsize::new( + WithdrawalBundleMetadata::MAX_SERIALIZED_SIZE + + RESPONSE_ENVELOPE_HEADROOM, + ) + .unwrap() + } +} + /// Request headers up to [`end`] #[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize)] pub struct GetHeadersRequest { @@ -114,6 +140,7 @@ impl PushTransactionRequest { pub enum Request { GetBlock(GetBlockRequest), GetHeaders(GetHeadersRequest), + GetWithdrawalBundle(GetWithdrawalBundleRequest), PushTransaction(PushTransactionRequest), } @@ -124,6 +151,7 @@ impl Request { match self { Self::GetBlock(request) => request.read_response_limit(), Self::GetHeaders(request) => request.read_response_limit(), + Self::GetWithdrawalBundle(request) => request.read_response_limit(), Self::PushTransaction(request) => request.read_response_limit(), } } @@ -141,6 +169,12 @@ impl From for Request { } } +impl From for Request { + fn from(request: GetWithdrawalBundleRequest) -> Self { + Self::GetWithdrawalBundle(request) + } +} + impl From for Request { fn from(request: PushTransactionRequest) -> Self { Self::PushTransaction(request) @@ -187,6 +221,7 @@ impl<'a> Serialize for RequestMessageRef<'a> { Heartbeat(&'b Heartbeat), GetBlock(&'b GetBlockRequest), GetHeaders(&'b GetHeadersRequest), + GetWithdrawalBundle(&'b GetWithdrawalBundleRequest), PushTransaction(&'b PushTransactionRequest), } @@ -197,6 +232,9 @@ impl<'a> Serialize for RequestMessageRef<'a> { RequestMessageRef::Request(request) => match request { Request::GetBlock(request) => Repr::GetBlock(request), Request::GetHeaders(request) => Repr::GetHeaders(request), + Request::GetWithdrawalBundle(request) => { + Repr::GetWithdrawalBundle(request) + } Request::PushTransaction(request) => { Repr::PushTransaction(request) } @@ -211,6 +249,7 @@ impl<'a> Serialize for RequestMessageRef<'a> { #[transitive( from(GetBlockRequest, Request), from(GetHeadersRequest, Request), + from(GetWithdrawalBundleRequest, Request), from(PushTransactionRequest, Request) )] pub enum RequestMessage { @@ -257,12 +296,14 @@ impl<'de> Deserialize<'de> for RequestMessage { Heartbeat(Heartbeat), GetBlock(GetBlockRequest), GetHeaders(GetHeadersRequest), + GetWithdrawalBundle(GetWithdrawalBundleRequest), PushTransaction(PushTransactionRequest), } let res = match Repr::deserialize(deserializer)? { Repr::Heartbeat(heartbeat) => heartbeat.into(), Repr::GetBlock(request) => request.into(), Repr::GetHeaders(request) => request.into(), + Repr::GetWithdrawalBundle(request) => request.into(), Repr::PushTransaction(request) => request.into(), }; Ok(res) @@ -284,8 +325,15 @@ pub enum ResponseMessage { NoHeader { block_hash: BlockHash, }, + NoWithdrawalBundle { + m6id: M6id, + }, TransactionAccepted(Txid), TransactionRejected(Txid), + WithdrawalBundle { + m6id: M6id, + metadata: WithdrawalBundleMetadata, + }, } impl ResponseMessage { diff --git a/lib/net/peer/mod.rs b/lib/net/peer/mod.rs index a9b31312..0e2f4aa4 100644 --- a/lib/net/peer/mod.rs +++ b/lib/net/peer/mod.rs @@ -10,6 +10,7 @@ use std::{ use bitcoin::Work; use borsh::BorshSerialize; use futures::channel::mpsc; +use parking_lot::RwLock; use quinn::{RecvStream, SendStream}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -370,6 +371,7 @@ impl Connection { pub struct ConnectionContext { pub env: sneed::Env, pub archive: Archive, + pub canonical_main_tip: Arc>>, pub network: Network, pub state: State, } diff --git a/lib/net/peer/request_queue.rs b/lib/net/peer/request_queue.rs index 78eaea34..0586be1f 100644 --- a/lib/net/peer/request_queue.rs +++ b/lib/net/peer/request_queue.rs @@ -24,6 +24,7 @@ const fn request_cost(req: &Request) -> NonZeroU32 { match req { Request::GetBlock { .. } => NonZeroU32::new(1000).unwrap(), Request::GetHeaders { .. } => NonZeroU32::new(10_000).unwrap(), + Request::GetWithdrawalBundle { .. } => NonZeroU32::new(1000).unwrap(), Request::PushTransaction { .. } => NonZeroU32::new(10).unwrap(), } } @@ -35,6 +36,7 @@ const fn request_cost(req: &Request) -> NonZeroU32 { pub struct ErrorRx { heartbeat_rx: mpsc::UnboundedReceiver, request_rx: mpsc::UnboundedReceiver, + request_hashes: Arc>>, rate_limiter: Arc, } @@ -44,6 +46,7 @@ impl ErrorRx { connection: Connection, peer_response_tx: mpsc::UnboundedSender, ) -> impl Stream { + let request_hashes = self.request_hashes; // Items in the combined source stream enum SourceItem { Error(error::channel_pool::SendMessage), @@ -117,6 +120,7 @@ impl ErrorRx { } } SourceItem::PeerResponse(peer_response) => { + request_hashes.lock().remove(&hash(&peer_response.request)); match peer_response_tx.unbounded_send(peer_response) { Ok(()) => None, Err(_err) => { @@ -170,15 +174,17 @@ impl Sender { pub fn new() -> (Sender, ErrorRx) { let (heartbeat_tx, heartbeat_rx) = mpsc::unbounded(); let (request_tx, request_rx) = mpsc::unbounded(); + let request_hashes = Arc::new(Mutex::new(HashSet::new())); let sender = Sender { heartbeat_tx, request_tx, - request_hashes: Arc::new(Mutex::new(HashSet::new())), + request_hashes: Arc::clone(&request_hashes), }; let rate_limiter = DefaultDirectRateLimiter::direct(REQUEST_QUOTA); let error_rx = ErrorRx { heartbeat_rx, request_rx, + request_hashes, rate_limiter: Arc::new(rate_limiter), }; (sender, error_rx) diff --git a/lib/net/peer/task.rs b/lib/net/peer/task.rs index 0d603ae1..5092f591 100644 --- a/lib/net/peer/task.rs +++ b/lib/net/peer/task.rs @@ -444,6 +444,9 @@ impl ConnectionTask { }; let tip_info = 'tip_info: { let rotxn = ctxt.env.read_txn()?; + let Some(main_tip) = *ctxt.canonical_main_tip.read() else { + break 'tip_info None; + }; let Some(tip) = ctxt.state.try_get_tip(&rotxn)? else { break 'tip_info None; }; @@ -451,8 +454,13 @@ impl ConnectionTask { .state .try_get_height(&rotxn)? .expect("Height should be known for tip"); - let bmm_verification = - ctxt.archive.get_best_main_verification(&rotxn, tip)?; + let Some(bmm_verification) = + ctxt.archive.try_get_best_main_verification_on_lineage( + &rotxn, tip, main_tip, + )? + else { + break 'tip_info None; + }; let total_work = ctxt.archive.get_total_work(&rotxn, bmm_verification)?; let tip = Tip { @@ -614,6 +622,25 @@ impl ConnectionTask { Ok(()) } + async fn handle_get_withdrawal_bundle( + ctxt: &ConnectionContext, + response_tx: SendStream, + m6id: crate::types::M6id, + ) -> Result<(), Error> { + let metadata = { + let rotxn = ctxt.env.read_txn().map_err(EnvError::from)?; + ctxt.state + .try_get_withdrawal_bundle_metadata(&rotxn, m6id)? + }; + let response = if let Some(metadata) = metadata { + ResponseMessage::WithdrawalBundle { m6id, metadata } + } else { + ResponseMessage::NoWithdrawalBundle { m6id } + }; + Connection::send_response(ctxt.network, response_tx, response).await?; + Ok(()) + } + fn handle_get_headers( ctxt: &ConnectionContext, forward_response_spawner: &join_set::Spawner, @@ -743,6 +770,12 @@ impl ConnectionTask { ); Ok(()) } + RequestMessage::Request(Request::GetWithdrawalBundle( + message::GetWithdrawalBundleRequest { m6id }, + )) => { + Self::handle_get_withdrawal_bundle(ctxt, response_tx, m6id) + .await + } RequestMessage::Request(Request::PushTransaction( message::PushTransactionRequest { transaction }, )) => { @@ -861,6 +894,10 @@ impl ConnectionTask { let tip_info = 'tip_info: { let rotxn = ctxt.env.read_txn().map_err(EnvError::from)?; + let Some(main_tip) = *ctxt.canonical_main_tip.read() + else { + break 'tip_info None; + }; let Some(tip) = ctxt.state.try_get_tip(&rotxn)? else { break 'tip_info None; }; @@ -868,9 +905,14 @@ impl ConnectionTask { .state .try_get_height(&rotxn)? .expect("Height for tip should be known"); - let bmm_verification = ctxt + let Some(bmm_verification) = ctxt .archive - .get_best_main_verification(&rotxn, tip)?; + .try_get_best_main_verification_on_lineage( + &rotxn, tip, main_tip, + )? + else { + break 'tip_info None; + }; let total_work = ctxt .archive .get_total_work(&rotxn, bmm_verification)?; diff --git a/lib/node/mainchain_task.rs b/lib/node/mainchain_task.rs index 51b76c2b..14437559 100644 --- a/lib/node/mainchain_task.rs +++ b/lib/node/mainchain_task.rs @@ -43,8 +43,16 @@ pub enum ResponseError { DbWrite(#[from] sneed::rwtxn::Error), #[error("CUSF Mainchain proto error")] Mainchain(#[from] proto::Error), + #[error("Mainchain block {0} was not available")] + MainchainBlockUnavailable(bitcoin::BlockHash), + #[error( + "Mainchain ancestor response for {0} changed while it was being synchronized" + )] + InconsistentMainchainAncestors(bitcoin::BlockHash), } +pub(super) type Event = Result; + /// Response indicating that a request has been fulfilled #[derive(Debug)] pub(super) enum Response { @@ -64,6 +72,12 @@ impl From<&Response> for Request { #[derive(Debug, Error)] enum Error { + #[error("CUSF mainchain event subscription error")] + SubscribeEvents(#[source] proto::Error), + #[error("CUSF mainchain event stream closed")] + EventStreamClosed, + #[error("Send mainchain event error")] + SendEvent, #[error("Send response error")] SendResponse(Box), #[error("Send response error (oneshot)")] @@ -78,11 +92,13 @@ struct MainchainTask { // instead of sending on `response_tx` request_rx: UnboundedReceiver<(Request, Option>)>, response_tx: UnboundedSender, + event_tx: UnboundedSender, } impl MainchainTask where Transport: proto::Transport, + proto::mainchain::ValidatorClient: Clone, { /// Request ancestor header info and block info from the mainchain node, /// including the specified header. @@ -104,14 +120,25 @@ where return Ok(true); } } + #[derive(Clone, Copy)] + struct AncestorRange { + newest: bitcoin::BlockHash, + exclusive_parent: bitcoin::BlockHash, + len: usize, + } + let mut current_block_hash = block_hash; let mut current_height = None; - let mut block_infos = - Vec::<(mainchain::BlockHeaderInfo, mainchain::BlockInfo)>::new(); + // Discover bounded batch ranges first. Keeping only two hashes and a + // length per range prevents initial sync from retaining every L1 + // BlockInfo while still allowing parent-first archive writes. + let mut ranges = Vec::::new(); tracing::debug!(%block_hash, "requesting ancestor headers/info"); const LOG_PROGRESS_INTERVAL: Duration = Duration::from_secs(5); const BATCH_REQUEST_SIZE: u32 = 1000; let mut progress_logged = Instant::now(); + let mut range_newest = current_block_hash; + let mut range_len = 0_usize; loop { if let Some(current_height) = current_height { let now = Instant::now(); @@ -124,79 +151,257 @@ where } tracing::trace!(%block_hash, "requesting ancestor headers: {current_block_hash}({current_height})") } - let Some(block_infos_resp) = cusf_mainchain - .get_block_infos(current_block_hash, BATCH_REQUEST_SIZE - 1) + let remaining_in_range = BATCH_REQUEST_SIZE as usize - range_len; + let Some(header_infos_resp) = cusf_mainchain + .get_block_header_infos( + current_block_hash, + u32::try_from(remaining_in_range - 1) + .expect("range capacity is at most 1000"), + ) .await? else { return Ok(false); }; + let response_len = 1 + header_infos_resp.tail.len(); + if header_infos_resp.head.block_hash != current_block_hash + || response_len > remaining_in_range { - let (current_header, _) = block_infos_resp.last(); - current_block_hash = current_header.prev_block_hash; - current_height = current_header.height.checked_sub(1); + return Err(ResponseError::InconsistentMainchainAncestors( + block_hash, + )); } - block_infos.extend(block_infos_resp); - if current_block_hash == bitcoin::BlockHash::all_zeros() { + range_len += response_len; + let oldest_header = header_infos_resp.last(); + let exclusive_parent = oldest_header.prev_block_hash; + current_block_hash = exclusive_parent; + current_height = oldest_header.height.checked_sub(1); + let reached_known_ancestor = + if current_block_hash == bitcoin::BlockHash::all_zeros() { + true + } else { + let rotxn = env.read_txn().map_err(EnvError::from)?; + archive + .try_get_main_header_info(&rotxn, ¤t_block_hash)? + .is_some() + }; + if range_len == BATCH_REQUEST_SIZE as usize + || reached_known_ancestor + { + ranges.push(AncestorRange { + newest: range_newest, + exclusive_parent, + len: range_len, + }); + range_newest = current_block_hash; + range_len = 0; + } + if reached_known_ancestor { break; } else { - let rotxn = env.read_txn().map_err(EnvError::from)?; - if archive - .try_get_main_header_info(&rotxn, ¤t_block_hash)? - .is_some() + debug_assert!(range_len < BATCH_REQUEST_SIZE as usize); + } + } + // Refetch and commit each bounded range, oldest first. A server may + // return fewer ancestors than requested, so fill the discovered range + // with as many bounded subrequests as necessary. + tracing::trace!(%block_hash, "storing ancestor headers/info"); + for range in ranges.into_iter().rev() { + let mut current_block_hash = range.newest; + let mut block_infos = Vec::with_capacity(range.len); + while block_infos.len() < range.len { + let remaining = range.len - block_infos.len(); + let max_ancestors = u32::try_from(remaining - 1) + .expect("discovered range length is at most 1000"); + let Some(block_infos_resp) = cusf_mainchain + .get_block_infos(current_block_hash, max_ancestors) + .await? + else { + return Ok(false); + }; + if block_infos_resp.head.0.block_hash != current_block_hash + || block_infos_resp.tail.len() + 1 > remaining { + return Err(ResponseError::InconsistentMainchainAncestors( + block_hash, + )); + } + let (oldest_header, _) = block_infos_resp.last(); + current_block_hash = oldest_header.prev_block_hash; + block_infos.extend(block_infos_resp); + if current_block_hash == range.exclusive_parent { + if block_infos.len() != range.len { + return Err( + ResponseError::InconsistentMainchainAncestors( + block_hash, + ), + ); + } break; } + if block_infos.len() == range.len + || current_block_hash == bitcoin::BlockHash::all_zeros() + { + return Err(ResponseError::InconsistentMainchainAncestors( + block_hash, + )); + } } - } - block_infos.reverse(); - // Writing all headers during IBD can starve archive readers. - tracing::trace!(%block_hash, "storing ancestor headers/info"); - task::block_in_place(|| { - let mut rwtxn = env.write_txn().map_err(EnvError::from)?; - for (header_info, block_info) in block_infos { - let () = + task::block_in_place(|| { + let mut rwtxn = env.write_txn().map_err(EnvError::from)?; + for (header_info, block_info) in block_infos.into_iter().rev() { archive.put_main_header_info(&mut rwtxn, &header_info)?; - let () = archive.put_main_block_info( - &mut rwtxn, - header_info.block_hash, - &block_info, - )?; + archive.put_main_block_info( + &mut rwtxn, + header_info.block_hash, + &block_info, + )?; + } + rwtxn.commit().map_err(RwTxnError::from)?; + Ok::<_, ResponseError>(()) + })?; + } + tracing::trace!(%block_hash, "stored ancestor headers/info"); + Ok(true) + } + + async fn prepare_event( + &mut self, + event: mainchain::Event, + ) -> Result { + match &event { + mainchain::Event::ConnectBlock { + header_info, + block_info, + } => { + let parent_available = Self::request_ancestor_infos( + &self.env, + &self.archive, + &mut self.mainchain, + header_info.prev_block_hash, + ) + .await?; + if !parent_available { + return Err(ResponseError::MainchainBlockUnavailable( + header_info.prev_block_hash, + )); + } + task::block_in_place(|| { + let mut rwtxn = + self.env.write_txn().map_err(EnvError::from)?; + self.archive + .put_main_header_info(&mut rwtxn, header_info)?; + self.archive.put_main_block_info( + &mut rwtxn, + header_info.block_hash, + block_info, + )?; + rwtxn.commit().map_err(RwTxnError::from)?; + Result::<(), ResponseError>::Ok(()) + })?; + } + mainchain::Event::DisconnectBlock { block_hash } => { + // Keep disconnected mainchain blocks in the archive. Their + // header is needed to identify the surviving parent, and the + // retained fork data may become canonical again later. + let block_available = Self::request_ancestor_infos( + &self.env, + &self.archive, + &mut self.mainchain, + *block_hash, + ) + .await?; + if !block_available { + return Err(ResponseError::MainchainBlockUnavailable( + *block_hash, + )); + } } - rwtxn.commit().map_err(RwTxnError::from)?; - tracing::trace!(%block_hash, "stored ancestor headers/info"); - Ok(true) + } + Ok(event) + } + + async fn current_tip_event( + &mut self, + ) -> Result { + let header_info = self.mainchain.get_chain_tip().await?; + let block_available = Self::request_ancestor_infos( + &self.env, + &self.archive, + &mut self.mainchain, + header_info.block_hash, + ) + .await?; + if !block_available { + return Err(ResponseError::MainchainBlockUnavailable( + header_info.block_hash, + )); + } + let block_info = task::block_in_place(|| { + let rotxn = self.env.read_txn().map_err(EnvError::from)?; + self.archive + .get_main_block_info(&rotxn, &header_info.block_hash) + .map_err(ResponseError::from) + })?; + Ok(mainchain::Event::ConnectBlock { + header_info, + block_info, }) } async fn run(mut self) -> Result<(), Error> { - while let Some((request, response_tx)) = self.request_rx.next().await { - match request { - Request::AncestorInfos(main_block_hash) => { - let res = Self::request_ancestor_infos( - &self.env, - &self.archive, - &mut self.mainchain, - main_block_hash, - ) - .await; - let response = - Response::AncestorInfos(main_block_hash, res); - if let Some(response_tx) = response_tx { - response_tx.send(response).map_err(|resp| { - Error::SendResponseOneshot(Box::new(resp)) - })?; - } else { - self.response_tx.unbounded_send(response).map_err( - |err| { - let resp = err.into_inner(); - Error::SendResponse(Box::new(resp)) - }, - )?; + let mut event_client = self.mainchain.clone(); + let mut events = event_client + .subscribe_events() + .await + .map_err(Error::SubscribeEvents)?; + let current_tip_event = self.current_tip_event().await; + self.event_tx + .unbounded_send(current_tip_event) + .map_err(|_| Error::SendEvent)?; + loop { + tokio::select! { + request = self.request_rx.next() => { + let Some((request, response_tx)) = request else { + return Ok(()); + }; + match request { + Request::AncestorInfos(main_block_hash) => { + let res = Self::request_ancestor_infos( + &self.env, + &self.archive, + &mut self.mainchain, + main_block_hash, + ) + .await; + let response = + Response::AncestorInfos(main_block_hash, res); + if let Some(response_tx) = response_tx { + response_tx.send(response).map_err(|resp| { + Error::SendResponseOneshot(Box::new(resp)) + })?; + } else { + self.response_tx.unbounded_send(response).map_err( + |err| { + let resp = err.into_inner(); + Error::SendResponse(Box::new(resp)) + }, + )?; + } + } } } + event = events.next() => { + let event = event.ok_or(Error::EventStreamClosed)?; + let event = match event { + Ok(event) => self.prepare_event(event).await, + Err(err) => Err(err.into()), + }; + self.event_tx + .unbounded_send(event) + .map_err(|_| Error::SendEvent)?; + } } } - Ok(()) } } @@ -216,20 +421,27 @@ impl MainchainTaskHandle { env: sneed::Env, archive: Archive, mainchain: mainchain::ValidatorClient, - ) -> (Self, mpsc::UnboundedReceiver) + ) -> ( + Self, + mpsc::UnboundedReceiver, + mpsc::UnboundedReceiver, + ) where Transport: proto::Transport + Send + 'static, + mainchain::ValidatorClient: Clone, >::Future: Send, { let (request_tx, request_rx) = mpsc::unbounded(); let (response_tx, response_rx) = mpsc::unbounded(); + let (event_tx, event_rx) = mpsc::unbounded(); let task = MainchainTask { env, archive, mainchain, request_rx, response_tx, + event_tx, }; let task = spawn(async move { if let Err(err) = task.run().await { @@ -241,7 +453,7 @@ impl MainchainTaskHandle { task: Arc::new(task), request_tx, }; - (task_handle, response_rx) + (task_handle, response_rx, event_rx) } /// Send a request diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 0365d205..abffdedb 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -26,9 +26,9 @@ use crate::{ Address, AmountOverflowError, AmountUnderflowError, AssetId, Authorized, AuthorizedTransaction, BitAssetData, BitAssetId, Block, BlockHash, BmmResult, Body, DutchAuctionId, FilledOutput, - FilledTransaction, GetBitcoinValue, Header, InPoint, Network, OutPoint, - OutPointKey, Output, SpentOutput, Tip, Transaction, TxIn, Txid, - WithdrawalBundle, + FilledTransaction, GetBitcoinValue, Header, InPoint, M6id, Network, + OutPoint, OutPointKey, Output, SpentOutput, Tip, Transaction, TxIn, + Txid, WithdrawalBundle, proto::{self, mainchain}, }, util::Watchable, @@ -58,6 +58,8 @@ pub enum Error { Archive(#[from] archive::Error), #[error("CUSF mainchain proto error")] CusfMainchain(#[from] proto::Error), + #[error("canonical mainchain tip is not initialized")] + CanonicalMainTipUnknown, #[error(transparent)] Db(#[from] DbError), #[error("Database env error")] @@ -190,12 +192,20 @@ where unsafe { env_open_opts.flags(fast_flags) }; unsafe { Env::open(&env_open_opts, &env_path) }? }; - let state = State::new(&env)?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Default::default())?; + + { + let rotxn = env.read_txn()?; + let tip = state.try_get_tip(&rotxn)?; + let accumulator = archive.accumulator_at(&rotxn, tip)?; + *state.utreexo_accumulator.lock() = accumulator; + } + #[cfg(feature = "zmq")] let zmq_pub_handler = Arc::new(ZmqPubHandler::new(zmq_addr).await?); - let archive = Archive::new(&env)?; let mempool = MemPool::new(&env)?; - let (mainchain_task, mainchain_task_response_rx) = + let (mainchain_task, mainchain_task_response_rx, mainchain_event_rx) = MainchainTaskHandle::new( env.clone(), archive.clone(), @@ -211,6 +221,7 @@ where archive.clone(), mainchain_task.clone(), mainchain_task_response_rx, + mainchain_event_rx, mempool.clone(), net.clone(), peer_info_rx, @@ -519,7 +530,7 @@ where .try_get(&rotxn, &outpoint_key) .map_err(state::Error::from)? { - spent.push((*outpoint, output)); + spent.push((*outpoint, output.output)); } } Ok(spent) @@ -564,9 +575,14 @@ where pub fn get_utxos_by_addresses( &self, addresses: &HashSet
, + height_threshold: u32, ) -> Result, Error> { let rotxn = self.env.read_txn()?; - let utxos = self.state.get_utxos_by_addresses(&rotxn, addresses)?; + let utxos = self.state.get_utxos_by_addresses( + &rotxn, + addresses, + height_threshold, + )?; Ok(utxos) } @@ -624,8 +640,13 @@ where hash: BlockHash, ) -> Result { let rotxn = self.env.read_txn()?; - let hash = self.archive.get_best_main_verification(&rotxn, hash)?; - Ok(hash) + let main_tip = self + .net + .canonical_main_tip() + .ok_or(Error::CanonicalMainTipUnknown)?; + self.archive + .try_get_best_main_verification_on_lineage(&rotxn, hash, main_tip)? + .ok_or_else(|| archive::Error::NoBmmResult(hash).into()) } pub fn get_bmm_inclusions( @@ -824,7 +845,17 @@ where pub fn connect_peer(&self, addr: SocketAddr) -> Result<(), Error> { self.net .connect_peer(self.env.clone(), addr) - .map_err(Error::from) + .map_err(Error::from)?; + for m6id in self.net_task.missing_withdrawal_bundles() { + self.net.request_withdrawal_bundle(m6id, Some(addr)); + } + Ok(()) + } + + /// Withdrawal bundle metadata currently preventing an atomic branch + /// replacement from being applied. + pub fn missing_withdrawal_bundles(&self) -> HashSet { + self.net_task.missing_withdrawal_bundles() } pub fn forget_peer(&self, addr: &SocketAddr) -> Result { diff --git a/lib/node/net_task.rs b/lib/node/net_task.rs index fac79081..14166d2b 100644 --- a/lib/node/net_task.rs +++ b/lib/node/net_task.rs @@ -1,15 +1,15 @@ //! Task to manage peers and their responses use std::{ - cmp::Ordering, collections::{HashMap, HashSet}, net::SocketAddr, sync::Arc, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; +use bitcoin::hashes::Hash as _; use error_fatality::{Nested as _, Split}; -use fallible_iterator::{FallibleIterator, IteratorExt}; +use fallible_iterator::FallibleIterator; use futures::{ StreamExt, channel::{ @@ -18,18 +18,22 @@ use futures::{ }, stream, }; -use nonempty::NonEmpty; +use parking_lot::RwLock; use sneed::{ DbError, EnvError, RwTxn, RwTxnError, db, env::error as env_error, rwtxn::error as rwtxn_error, }; use thiserror::Error; use tokio::task::{self, JoinHandle}; -use tokio_stream::StreamNotifyClose; +use tokio_stream::{StreamNotifyClose, wrappers::IntervalStream}; use super::mainchain_task::{self, MainchainTaskHandle}; use crate::{ - archive::{self, Archive}, + archive::{ + self, ACCUMULATOR_PRUNE_INTERVAL_BLOCKS, + ACCUMULATOR_REORG_HORIZON_BLOCKS, ACCUMULATOR_SNAPSHOT_INTERVAL_BLOCKS, + Archive, TXDB_PRUNE_INTERVAL_BLOCKS, TXDB_RETENTION_SECS, + }, mempool::{self, MemPool}, net::{ self, Net, PeerConnectionError, PeerConnectionInfo, @@ -38,12 +42,15 @@ use crate::{ }, state::{self, State}, types::{ - BmmResult, Body, Header, Tip, + Accumulator, AccumulatorDiff, BlockHash, Body, Header, InPoint, M6id, + OutPoint, Tip, proto::{self, mainchain}, }, util::{ErrorChain, join_set}, }; +const WITHDRAWAL_METADATA_RETRY_INTERVAL: Duration = Duration::from_secs(30); + #[allow(clippy::duplicated_attributes)] #[derive(transitive::Transitive, Debug, Error)] #[transitive( @@ -65,6 +72,42 @@ pub enum Error { DbWrite(#[from] RwTxnError), #[error("Forward mainchain task request failed")] ForwardMainchainTaskRequest, + #[error("mainchain event error")] + MainchainEvent(#[source] Box), + #[error("mainchain event stream closed")] + MainchainEventRxClosed, + #[error( + "raw mainchain disconnect requires {disconnects} sidechain disconnects, exceeding the retained accumulator horizon of {max_disconnects}" + )] + MainchainDisconnectBeyondHorizon { + disconnects: u64, + max_disconnects: u32, + }, + #[error( + "raw mainchain disconnect requires rewinding to sidechain height {target_height:?}, below the retained accumulator floor {retained_floor}" + )] + MainchainDisconnectBelowAccumulatorFloor { + target_height: Option, + retained_floor: u32, + }, + #[error("state error while reconciling with the canonical mainchain")] + MainchainReconciliationState(#[source] Box), + #[error( + "sync blocked waiting for withdrawal bundle metadata for {m6id}, referenced by mainchain block {event_block_hash}" + )] + MissingWithdrawalBundleMetadata { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + }, + #[error("invalid block")] + InvalidBlock(#[source] Box), + #[error( + "mainchain interval start {start:?} is not an ancestor of interval end {end}" + )] + InvalidMainchainInterval { + start: bitcoin::BlockHash, + end: bitcoin::BlockHash, + }, #[error("mempool error")] MemPool(#[from] mempool::Error), #[error("Net error")] @@ -83,6 +126,8 @@ pub enum Error { SendReorgResultOneshot, #[error("state error")] State(#[from] Box), + #[error(transparent)] + Utreexo(#[from] crate::types::UtreexoError), } impl From for Error { @@ -137,34 +182,303 @@ impl From for Error { } } -fn connect_tip_( +fn unix_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |duration| duration.as_secs()) +} + +/// Return the hashes in `(exclusive_start, inclusive_end]`, newest-to-oldest. +/// +/// Keeping only hashes here is important: L1 intervals are not bounded by the +/// sidechain reorg horizon, and `BlockInfo` can contain arbitrarily many +/// deposits and withdrawal events. +fn mainchain_interval( + archive: &Archive, + rotxn: &sneed::RoTxn, + exclusive_start: Option, + inclusive_end: bitcoin::BlockHash, +) -> Result, Error> { + let exclusive_start = exclusive_start + .filter(|block_hash| *block_hash != bitcoin::BlockHash::all_zeros()); + if exclusive_start == Some(inclusive_end) { + return Ok(Vec::new()); + } + let mut interval = Vec::new(); + let mut ancestors = archive.main_ancestors(rotxn, inclusive_end); + while let Some(block_hash) = ancestors.next()? { + if Some(block_hash) == exclusive_start { + return Ok(interval); + } + interval.push(block_hash); + } + if let Some(start) = exclusive_start { + return Err(Error::InvalidMainchainInterval { + start, + end: inclusive_end, + }); + } + Ok(interval) +} + +fn state_error_is_infrastructure(err: &state::Error) -> bool { + matches!( + err, + state::Error::Db(_) + | state::Error::BorshSerialize(_) + | state::Error::Amm(state::error::Amm::Db(_)) + | state::Error::BitAsset(state::error::BitAsset::Db(_)) + | state::Error::DutchAuction(state::error::DutchAuction::Db(_)) + | state::Error::ConnectWithdrawalBundleSubmitted( + state::error::ConnectWithdrawalBundleSubmitted::Db(_) + ) + ) +} + +fn classify_prevalidation_error(err: state::Error) -> Error { + if state_error_is_infrastructure(&err) { + Error::State(Box::new(err)) + } else { + Error::InvalidBlock(Box::new(err)) + } +} + +fn classify_two_way_peg_error(err: state::Error) -> Error { + if let Some((event_block_hash, m6id)) = + err.missing_withdrawal_bundle_metadata() + { + Error::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + } + } else { + Error::State(Box::new(err)) + } +} + +fn classify_mainchain_reconciliation_error(err: Error) -> Error { + match err { + Error::State(err) => Error::MainchainReconciliationState(err), + err => err, + } +} + +fn accumulator_common_is_retained( + common_ancestor_height: Option, + retained_reorg_floor: u32, +) -> bool { + common_ancestor_height.is_some_and(|height| height >= retained_reorg_floor) + || (common_ancestor_height.is_none() && retained_reorg_floor == 0) +} + +fn accumulator_reorg_is_within_horizon(disconnects: u64) -> bool { + disconnects <= u64::from(ACCUMULATOR_REORG_HORIZON_BLOCKS) +} + +/// Return the newest verified BMM inclusion for `side_block` that is still on +/// the ancestry of the specified canonical mainchain tip. +fn best_main_verification_on_lineage( + archive: &Archive, + rotxn: &sneed::RoTxn, + side_block: BlockHash, + main_tip: bitcoin::BlockHash, +) -> Result, Error> { + Ok(archive.try_get_best_main_verification_on_lineage( + rotxn, side_block, main_tip, + )?) +} + +fn tip_for_side_block( + archive: &Archive, + rotxn: &sneed::RoTxn, + side_block: BlockHash, + main_tip: Option, +) -> Result, Error> { + let main_block_hash = if let Some(main_tip) = main_tip { + best_main_verification_on_lineage(archive, rotxn, side_block, main_tip)? + } else { + archive.try_get_best_main_verification(rotxn, side_block)? + }; + Ok(main_block_hash.map(|main_block_hash| Tip { + block_hash: side_block, + main_block_hash, + })) +} + +fn side_tip_on_mainchain_lineage( + archive: &Archive, + rotxn: &sneed::RoTxn, + current_tip: BlockHash, + main_tip: bitcoin::BlockHash, +) -> Result, Error> { + let mut ancestors = archive.ancestors(rotxn, current_tip); + while let Some(side_block) = ancestors.next()? { + if best_main_verification_on_lineage( + archive, rotxn, side_block, main_tip, + )? + .is_some() + { + return Ok(Some(side_block)); + } + } + Ok(None) +} + +#[allow(clippy::too_many_arguments)] +fn connect_tip_with_two_way_peg_( rwtxn: &mut RwTxn<'_>, archive: &Archive, mempool: &MemPool, state: &State, header: &Header, body: &Body, - two_way_peg_data: &mainchain::TwoWayPegData, -) -> Result<(), Error> { + connect_two_way_peg: F, + accumulator: &mut Accumulator, +) -> Result<(), Error> +where + F: FnOnce(&mut RwTxn<'_>) -> Result, +{ let block_hash = header.hash(); + let prevalidated = state + .prevalidate_block(rwtxn, header, body) + .map_err(classify_prevalidation_error)?; + let block_height = prevalidated.next_height; + let merkle_root = prevalidated.computed_merkle_root; + let filled_txs = &prevalidated.filled_transactions; + let mut accumulator_diff = prevalidated.accumulator_diff.clone(); + state.connect_prevalidated_block(rwtxn, header, body, &prevalidated)?; if tracing::enabled!(tracing::Level::DEBUG) { - let merkle_root = - Body::compute_merkle_root(&body.coinbase, &body.transactions); - let height = state.try_get_height(rwtxn)?; - state.apply_block(rwtxn, header, body)?; - tracing::debug!(?height, %merkle_root, %block_hash, "connected body") - } else { - state.apply_block(rwtxn, header, body)?; + tracing::debug!(height = block_height, %merkle_root, %block_hash, "connected body") } - let () = state.connect_two_way_peg_data(rwtxn, two_way_peg_data)?; + let peg_accumulator_diff = connect_two_way_peg(rwtxn)?; + accumulator_diff.merge(peg_accumulator_diff); + archive.put_accumulator_diff(rwtxn, block_hash, &accumulator_diff)?; let () = archive.put_header(rwtxn, header)?; let () = archive.put_body(rwtxn, block_hash, body)?; + let unix_stamp = unix_now(); + let () = archive.put_txdb_for_connected_block( + rwtxn, + block_hash, + block_height, + body, + filled_txs, + unix_stamp, + )?; + if block_height > 0 && block_height % TXDB_PRUNE_INTERVAL_BLOCKS == 0 { + let cutoff_unix = unix_stamp.saturating_sub(TXDB_RETENTION_SECS); + archive.prune_txdb_older_than(rwtxn, cutoff_unix)?; + } + accumulator.apply_diff(accumulator_diff)?; + if block_height > 0 + && block_height % ACCUMULATOR_SNAPSHOT_INTERVAL_BLOCKS == 0 + { + archive.put_accumulator( + rwtxn, + block_hash, + block_height, + accumulator, + )?; + } + if block_height > 0 && block_height % ACCUMULATOR_PRUNE_INTERVAL_BLOCKS == 0 + { + let reorg_floor = + block_height.saturating_sub(ACCUMULATOR_REORG_HORIZON_BLOCKS); + let snapshot_anchor = + reorg_floor - reorg_floor % ACCUMULATOR_SNAPSHOT_INTERVAL_BLOCKS; + archive.prune_accumulator_older_than(rwtxn, snapshot_anchor)?; + archive.prune_accumulator_diffs_older_than(rwtxn, snapshot_anchor)?; + archive.advance_accumulator_reorg_floor(rwtxn, snapshot_anchor)?; + } for transaction in &body.transactions { + for input in &transaction.inputs { + if let Some(InPoint::Regular { txid, .. }) = mempool + .spent_utxos + .try_get(rwtxn, input) + .map_err(DbError::from)? + { + // A newly connected block wins over conflicting mempool + // transactions and all of their descendants. + mempool.delete(rwtxn, txid)?; + } + } let () = mempool.delete(rwtxn, transaction.txid())?; } Ok(()) } +#[allow(clippy::too_many_arguments)] +#[cfg(test)] +fn connect_tip_( + rwtxn: &mut RwTxn<'_>, + archive: &Archive, + mempool: &MemPool, + state: &State, + header: &Header, + body: &Body, + two_way_peg_data: &mainchain::TwoWayPegData, + accumulator: &mut Accumulator, +) -> Result<(), Error> { + connect_tip_with_two_way_peg_( + rwtxn, + archive, + mempool, + state, + header, + body, + |rwtxn| { + state + .connect_two_way_peg_data(rwtxn, two_way_peg_data) + .map_err(classify_two_way_peg_error) + }, + accumulator, + ) +} + +#[allow(clippy::too_many_arguments)] +fn connect_tip_with_mainchain_blocks_( + rwtxn: &mut RwTxn<'_>, + archive: &Archive, + mempool: &MemPool, + state: &State, + header: &Header, + body: &Body, + mainchain_blocks: &[bitcoin::BlockHash], + accumulator: &mut Accumulator, +) -> Result<(), Error> { + connect_tip_with_two_way_peg_( + rwtxn, + archive, + mempool, + state, + header, + body, + |rwtxn| { + let mut context = state + .begin_connect_two_way_peg_data(rwtxn) + .map_err(classify_two_way_peg_error)?; + // The hashes are oldest-to-newest, matching `TwoWayPegData`'s + // connection order. Deserialize and discard one BlockInfo at a + // time so the interval does not determine peak memory. + for block_hash in mainchain_blocks { + let block_info = + archive.get_main_block_info(rwtxn, block_hash)?; + state + .connect_two_way_peg_block_info( + rwtxn, + *block_hash, + &block_info, + &mut context, + ) + .map_err(classify_two_way_peg_error)?; + } + state + .finish_connect_two_way_peg_data(rwtxn, context) + .map_err(classify_two_way_peg_error) + }, + accumulator, + ) +} + fn disconnect_tip_( rwtxn: &mut RwTxn<'_>, archive: &Archive, @@ -175,92 +489,71 @@ fn disconnect_tip_( state.try_get_tip(rwtxn)?.ok_or(state::Error::NoTip)?; let tip_header = archive.get_header(rwtxn, tip_block_hash)?; let tip_body = archive.get_body(rwtxn, tip_block_hash)?; - let height = state.try_get_height(rwtxn)?.ok_or(state::Error::NoTip)?; - let two_way_peg_data = { - let last_applied_deposit_block = state - .deposit_blocks() - .rev_iter(rwtxn) - .map_err(DbError::from)? - .find_map(|(_, (block_hash, applied_height))| { - if applied_height < height - 1 { - Ok(Some((block_hash, applied_height))) - } else { - Ok(None) - } - })?; - let last_applied_withdrawal_bundle_event_block = state - .withdrawal_bundle_event_blocks() - .rev_iter(rwtxn)? - .find_map(|(_, (block_hash, applied_height))| { - if applied_height < height - 1 { - Ok(Some((block_hash, applied_height))) - } else { - Ok(None) - } - }) - .map_err(DbError::from)?; - let start_block_hash = match ( - last_applied_deposit_block, - last_applied_withdrawal_bundle_event_block, - ) { - (None, None) => None, - (Some((block_hash, _)), None) | (None, Some((block_hash, _))) => { - Some(block_hash) + // A side block applies exactly the mainchain interval after its parent's + // frontier through its own frontier. Derive that interval from headers, + // then reverse it one BlockInfo at a time. + let start_block_hash = tip_header + .prev_side_hash + .map(|parent| archive.get_header(rwtxn, parent)) + .transpose()? + .map(|parent| parent.prev_main_hash); + let mainchain_blocks = mainchain_interval( + archive, + rwtxn, + start_block_hash, + tip_header.prev_main_hash, + )?; + let mut context = state.begin_disconnect_two_way_peg_data(rwtxn)?; + // `mainchain_interval` is already newest-to-oldest. + for block_hash in mainchain_blocks { + let block_info = archive.get_main_block_info(rwtxn, &block_hash)?; + state.disconnect_two_way_peg_block_info( + rwtxn, + block_hash, + &block_info, + &mut context, + )?; + } + state.finish_disconnect_two_way_peg_data(rwtxn, context)?; + archive.prune_txdb_block(rwtxn, tip_block_hash)?; + let () = state.disconnect_tip(rwtxn, &tip_header, &tip_body)?; + for transaction in tip_body.authorized_transactions() { + let mut conflicts = false; + for input in &transaction.transaction.inputs { + if mempool + .spent_utxos + .try_get(rwtxn, input) + .map_err(DbError::from)? + .is_some() + { + conflicts = true; + break; } - ( - Some((deposit_block, deposit_block_applied_height)), - Some(( - withdrawal_event_block, - withdrawal_event_block_applied_height, - )), - ) => { - match deposit_block_applied_height - .cmp(&withdrawal_event_block_applied_height) + } + if conflicts { + let txid = transaction.transaction.txid(); + for vout in 0..transaction.transaction.outputs.len() { + let outpoint = OutPoint::Regular { + txid, + vout: vout as u32, + }; + if let Some(InPoint::Regular { + txid: child_txid, .. + }) = mempool + .spent_utxos + .try_get(rwtxn, &outpoint) + .map_err(DbError::from)? { - Ordering::Less => Some(withdrawal_event_block), - Ordering::Greater => Some(deposit_block), - Ordering::Equal => { - if archive.is_main_descendant( - rwtxn, - withdrawal_event_block, - deposit_block, - )? { - Some(withdrawal_event_block) - } else { - assert!(archive.is_main_descendant( - rwtxn, - deposit_block, - withdrawal_event_block - )?); - Some(deposit_block) - } - } + mempool.delete(rwtxn, child_txid)?; } } - }; - let block_infos: Vec<_> = archive - .main_ancestors(rwtxn, tip_header.prev_main_hash) - .take_while(|ancestor| { - Ok(Some(ancestor) != start_block_hash.as_ref()) - }) - .filter_map(|ancestor| { - let block_info = - archive.get_main_block_info(rwtxn, &ancestor)?; - if block_info.events.is_empty() { - Ok(None) - } else { - Ok(Some((ancestor, block_info))) - } - }) - .collect()?; - mainchain::TwoWayPegData { - block_info: block_infos.into_iter().rev().collect(), + tracing::debug!( + %txid, + "not restoring disconnected transaction due to a mempool conflict" + ); + } else { + mempool.put(rwtxn, &transaction)?; } - }; - let () = state.disconnect_two_way_peg_data(rwtxn, &two_way_peg_data)?; - let () = state.disconnect_tip(rwtxn, &tip_header, &tip_body)?; - for transaction in tip_body.authorized_transactions().iter().rev() { - mempool.put(rwtxn, transaction)?; } Ok(()) } @@ -269,33 +562,321 @@ fn disconnect_tip_( /// The new tip block and all ancestor blocks must exist in the node's archive. /// A result of `Ok(true)` indicates a successful re-org. /// A result of `Ok(false)` indicates that no re-org was attempted. -// a state error means a peer sent an invalid block; it must not be fatal +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ReorgErrorClass { + Blocked, + Fatal, + InvalidBlock, + Rejected, +} + +fn reorg_error_class(err: &Error) -> ReorgErrorClass { + match err { + Error::MissingWithdrawalBundleMetadata { .. } => { + ReorgErrorClass::Blocked + } + Error::InvalidBlock(_) => ReorgErrorClass::InvalidBlock, + Error::State(err) if !state_error_is_infrastructure(err) => { + ReorgErrorClass::Rejected + } + _ => ReorgErrorClass::Fatal, + } +} + +#[cfg(test)] fn is_fatal_reorg_error(err: &Error) -> bool { - !matches!(err, Error::State(_)) + reorg_error_class(err) == ReorgErrorClass::Fatal +} + +fn replace_accumulator_at( + archive: &Archive, + rotxn: &sneed::RoTxn, + target: Option, + accumulator: &mut Accumulator, +) -> Result<(), Error> { + drop(std::mem::take(accumulator)); + *accumulator = archive.accumulator_at(rotxn, target)?; + Ok(()) +} + +fn restore_committed_accumulator( + env: &sneed::Env, + archive: &Archive, + state: &State, + accumulator: &mut Accumulator, +) -> Result<(), Error> { + let rotxn = env.read_txn().map_err(EnvError::from)?; + let committed_tip = state.try_get_tip(&rotxn)?; + replace_accumulator_at(archive, &rotxn, committed_tip, accumulator) +} + +fn with_accumulator_recovery( + env: &sneed::Env, + archive: &Archive, + state: &State, + accumulator: &mut Accumulator, + operation: impl FnOnce(&mut Accumulator) -> Result, +) -> Result { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + operation(accumulator) + })); + match result { + Ok(result) => { + if result.is_err() + && let Err(restore_err) = restore_committed_accumulator( + env, + archive, + state, + accumulator, + ) + { + tracing::error!( + err = format!("{restore_err:#}"), + "failed to restore accumulator after aborted reorg" + ); + return Err(restore_err); + } + result + } + Err(panic) => { + if let Err(restore_err) = + restore_committed_accumulator(env, archive, state, accumulator) + { + tracing::error!( + err = format!("{restore_err:#}"), + "failed to restore accumulator after panicked reorg" + ); + } + std::panic::resume_unwind(panic) + } + } +} + +fn disconnect_for_mainchain_tip( + env: &sneed::Env, + archive: &Archive, + mempool: &MemPool, + state: &State, + main_tip: bitcoin::BlockHash, +) -> Result { + let mut accumulator = state.utreexo_accumulator.lock(); + with_accumulator_recovery( + env, + archive, + state, + &mut accumulator, + |accumulator| { + disconnect_for_mainchain_tip_inner( + env, + archive, + mempool, + state, + main_tip, + accumulator, + ) + }, + ) + .map_err(classify_mainchain_reconciliation_error) +} + +fn disconnect_for_mainchain_tip_inner( + env: &sneed::Env, + archive: &Archive, + mempool: &MemPool, + state: &State, + main_tip: bitcoin::BlockHash, + accumulator: &mut Accumulator, +) -> Result { + let mut rwtxn = env.write_txn().map_err(EnvError::from)?; + let Some(current_tip) = state.try_get_tip(&rwtxn)? else { + return Ok(false); + }; + let target = + side_tip_on_mainchain_lineage(archive, &rwtxn, current_tip, main_tip)?; + if target == Some(current_tip) { + return Ok(false); + } + + let current_height = + state.try_get_height(&rwtxn)?.ok_or(state::Error::NoTip)?; + let target_height = target + .map(|target| archive.get_height(&rwtxn, target)) + .transpose()?; + let disconnects = target_height + .map_or(u64::from(current_height) + 1, |target_height| { + u64::from(current_height - target_height) + }); + if !accumulator_reorg_is_within_horizon(disconnects) { + return Err(Error::MainchainDisconnectBeyondHorizon { + disconnects, + max_disconnects: ACCUMULATOR_REORG_HORIZON_BLOCKS, + }); + } + let retained_floor = archive.accumulator_reorg_floor(&rwtxn)?; + if !accumulator_common_is_retained(target_height, retained_floor) { + return Err(Error::MainchainDisconnectBelowAccumulatorFloor { + target_height, + retained_floor, + }); + } + + tracing::info!( + %current_tip, + ?target, + %main_tip, + disconnects, + "rewinding sidechain after mainchain disconnect" + ); + replace_accumulator_at(archive, &rwtxn, target, accumulator)?; + for _ in 0..disconnects { + disconnect_tip_(&mut rwtxn, archive, mempool, state)?; + } + assert_eq!(state.try_get_tip(&rwtxn)?, target); + rwtxn.commit().map_err(RwTxnError::from)?; + Ok(true) +} + +fn ready_mainchain_candidate( + archive: &Archive, + state: &State, + rotxn: &sneed::RoTxn, + side_block: BlockHash, + main_tip: bitcoin::BlockHash, +) -> Result, Error> { + if archive.try_get_header(rotxn, side_block)?.is_none() { + return Ok(None); + } + let Some(main_block_hash) = best_main_verification_on_lineage( + archive, rotxn, side_block, main_tip, + )? + else { + return Ok(None); + }; + let current_tip = state.try_get_tip(rotxn)?; + let common_ancestor = current_tip + .map(|current_tip| { + archive.last_common_ancestor(rotxn, current_tip, side_block) + }) + .transpose()? + .flatten(); + if !archive + .get_missing_bodies(rotxn, side_block, common_ancestor)? + .is_empty() + { + return Ok(None); + } + Ok(Some(Tip { + block_hash: side_block, + main_block_hash, + })) +} + +/// Find the best fully archived sidechain tip verified anywhere on the +/// specified canonical mainchain lineage. Used for startup reconciliation, +/// where intermediate mainchain connect events may have happened while this +/// node was offline. +fn best_ready_mainchain_candidate( + archive: &Archive, + state: &State, + rotxn: &sneed::RoTxn, + main_tip: bitcoin::BlockHash, +) -> Result, Error> { + let mut best = None; + for verified_tip in archive.get_mainchain_verified_tips(rotxn, main_tip)? { + let Some(candidate) = ready_mainchain_candidate( + archive, + state, + rotxn, + verified_tip.block_hash, + main_tip, + )? + else { + continue; + }; + best = match best { + None => Some(candidate), + Some(current_best) => archive + .better_tip(rotxn, current_best, candidate)? + .or(Some(current_best)), + }; + } + Ok(best) } -fn reorg_to_tip( +fn reorg_to_tip_on_mainchain( env: &sneed::Env, archive: &Archive, mempool: &MemPool, state: &State, #[cfg(feature = "zmq")] zmq_pub_handler: &ZmqPubHandler, new_tip: Tip, + main_tip: bitcoin::BlockHash, +) -> Result { + // Keep accumulator readers blocked until the database and forest agree. + // If the database transaction aborts, reconstruct the forest from the + // committed tip before releasing the lock. + let mut accumulator = state.utreexo_accumulator.lock(); + with_accumulator_recovery( + env, + archive, + state, + &mut accumulator, + |accumulator| { + reorg_to_tip_inner( + env, + archive, + mempool, + state, + #[cfg(feature = "zmq")] + zmq_pub_handler, + new_tip, + main_tip, + accumulator, + ) + }, + ) +} + +#[allow(clippy::too_many_arguments)] +fn reorg_to_tip_inner( + env: &sneed::Env, + archive: &Archive, + mempool: &MemPool, + state: &State, + #[cfg(feature = "zmq")] zmq_pub_handler: &ZmqPubHandler, + mut new_tip: Tip, + main_tip: bitcoin::BlockHash, + accumulator: &mut Accumulator, ) -> Result { let mut rwtxn = env.write_txn().map_err(EnvError::from)?; + let Some(main_block_hash) = best_main_verification_on_lineage( + archive, + &rwtxn, + new_tip.block_hash, + main_tip, + )? + else { + tracing::debug!(?new_tip, %main_tip, "candidate tip is not verified on the current mainchain"); + return Ok(false); + }; + new_tip.main_block_hash = main_block_hash; let tip_height = state.try_get_height(&rwtxn)?; - let tip = state - .try_get_tip(&rwtxn)? + let tip_hash = state.try_get_tip(&rwtxn)?; + let comparable_tip = tip_hash .map(|tip_hash| { - let bmm_verification = - archive.get_best_main_verification(&rwtxn, tip_hash)?; - Ok::<_, Error>(Tip { + let bmm_verification = best_main_verification_on_lineage( + archive, &rwtxn, tip_hash, main_tip, + )?; + Ok::<_, Error>(bmm_verification.map(|main_block_hash| Tip { block_hash: tip_hash, - main_block_hash: bmm_verification, - }) + main_block_hash, + })) }) - .transpose()?; - if let Some(tip) = tip { + .transpose()? + .flatten(); + let mainchain_reconciliation_required = + tip_hash.is_some() && comparable_tip.is_none(); + if let Some(tip) = comparable_tip { // check that new tip is better than current tip if archive.better_tip(&rwtxn, tip, new_tip)? != Some(new_tip) { tracing::debug!( @@ -306,61 +887,80 @@ fn reorg_to_tip( return Ok(false); } } - let common_ancestor = if let Some(tip) = tip { - archive.last_common_ancestor( - &rwtxn, - tip.block_hash, - new_tip.block_hash, - )? + let common_ancestor = if let Some(tip_hash) = tip_hash { + archive.last_common_ancestor(&rwtxn, tip_hash, new_tip.block_hash)? } else { None }; - // Check that all necessary bodies exist before disconnecting tip - let blocks_to_apply: NonEmpty<(Header, Body)> = { - let header = archive.get_header(&rwtxn, new_tip.block_hash)?; - let body = archive.get_body(&rwtxn, new_tip.block_hash)?; - let ancestors = if let Some(prev_side_hash) = header.prev_side_hash { - archive - .ancestors(&rwtxn, prev_side_hash) - .take_while(|block_hash| { - Ok(common_ancestor.is_none_or(|common_ancestor| { - *block_hash != common_ancestor - })) - }) - .map(|block_hash| { - let header = archive.get_header(&rwtxn, block_hash)?; - let body = archive.get_body(&rwtxn, block_hash)?; - Ok((header, body)) - }) - .collect()? - } else { - Vec::new() - }; - NonEmpty { - head: (header, body), - tail: ancestors, + let common_ancestor_height = common_ancestor + .map(|common_ancestor| archive.get_height(&rwtxn, common_ancestor)) + .transpose()?; + let disconnects = tip_height.map_or(0_u64, |tip_height| { + common_ancestor_height.map_or(u64::from(tip_height) + 1, |height| { + u64::from(tip_height - height) + }) + }); + if !accumulator_reorg_is_within_horizon(disconnects) { + if mainchain_reconciliation_required { + return Err(Error::MainchainDisconnectBeyondHorizon { + disconnects, + max_disconnects: ACCUMULATOR_REORG_HORIZON_BLOCKS, + }); } - }; - // Disconnect tip until common ancestor is reached - let mut common_ancestor_height = None; - if let Some(tip_height) = tip_height { - if let Some(common_ancestor) = common_ancestor { - common_ancestor_height = - Some(archive.get_height(&rwtxn, common_ancestor)?); + tracing::warn!( + ?new_tip, + disconnects, + max_disconnects = ACCUMULATOR_REORG_HORIZON_BLOCKS, + "refusing reorg beyond the retained accumulator horizon" + ); + return Ok(false); + } + let retained_reorg_floor = archive.accumulator_reorg_floor(&rwtxn)?; + let common_ancestor_is_retained = accumulator_common_is_retained( + common_ancestor_height, + retained_reorg_floor, + ); + if disconnects != 0 && !common_ancestor_is_retained { + if mainchain_reconciliation_required { + return Err(Error::MainchainDisconnectBelowAccumulatorFloor { + target_height: common_ancestor_height, + retained_floor: retained_reorg_floor, + }); } + tracing::warn!( + ?new_tip, + ?common_ancestor, + ?common_ancestor_height, + retained_reorg_floor, + "refusing reorg below the retained accumulator floor" + ); + return Ok(false); + } + // Keep only hashes here. Replacement branches can be much longer than the + // disconnect horizon, so retaining every deserialized body would make peak + // memory proportional to the entire branch. + let blocks_to_apply: Vec = archive + .ancestors(&rwtxn, new_tip.block_hash) + .take_while(|block_hash| { + Ok(common_ancestor + .is_none_or(|common_ancestor| *block_hash != common_ancestor)) + }) + .collect()?; + // Check that all necessary bodies exist before disconnecting tip, while + // dropping each deserialized body before loading the next one. + for block_hash in &blocks_to_apply { + drop(archive.get_body(&rwtxn, *block_hash)?); + } + // Disconnect tip until common ancestor is reached + if disconnects != 0 { tracing::debug!( - ?tip, + ?tip_hash, ?tip_height, ?common_ancestor, ?common_ancestor_height, "Disconnecting tip until common ancestor is reached" ); - let disconnects = - if let Some(common_ancestor_height) = common_ancestor_height { - tip_height - common_ancestor_height - } else { - tip_height + 1 - }; + replace_accumulator_at(archive, &rwtxn, common_ancestor, accumulator)?; for _ in 0..disconnects { let () = disconnect_tip_(&mut rwtxn, archive, mempool, state)?; } @@ -369,53 +969,63 @@ fn reorg_to_tip( let tip_hash = state.try_get_tip(&rwtxn)?; assert_eq!(tip_hash, common_ancestor); } - let mut two_way_peg_data_batch: Vec<_> = { - let common_ancestor_header = - if let Some(common_ancestor) = common_ancestor { - Some(archive.get_header(&rwtxn, common_ancestor)?) - } else { - None - }; - let common_ancestor_prev_main_hash = - common_ancestor_header.map(|header| header.prev_main_hash); - archive - .main_ancestors(&rwtxn, blocks_to_apply.head.0.prev_main_hash) - .take_while(|ancestor| { - Ok(Some(ancestor) != common_ancestor_prev_main_hash.as_ref()) - }) - .map(|ancestor| { - let block_info = - archive.get_main_block_info(&rwtxn, &ancestor)?; - Ok((ancestor, block_info)) - }) - .collect()? - }; - // Apply blocks until new tip is reached - for (header, body) in blocks_to_apply.iter().rev() { - let two_way_peg_data = { - let mut two_way_peg_data = mainchain::TwoWayPegData::default(); - 'fill_2wpd: while let Some((block_hash, block_info)) = - two_way_peg_data_batch.pop() - { - two_way_peg_data.block_info.replace(block_hash, block_info); - if block_hash == header.prev_main_hash { - break 'fill_2wpd; + let common_ancestor_prev_main_hash = common_ancestor + .map(|common_ancestor| archive.get_header(&rwtxn, common_ancestor)) + .transpose()? + .map(|header| header.prev_main_hash); + let mut mainchain_block_batch = + if let Some(block_hash) = blocks_to_apply.first() { + let header = archive.get_header(&rwtxn, *block_hash)?; + mainchain_interval( + archive, + &rwtxn, + common_ancestor_prev_main_hash, + header.prev_main_hash, + )? + } else { + Vec::new() + }; + let mut applied_main_frontier = common_ancestor_prev_main_hash + .unwrap_or_else(bitcoin::BlockHash::all_zeros); + // Apply every replacement block in the same transaction as the + // disconnects. Committing inside this loop would expose a partial reorg. + for block_hash in blocks_to_apply.iter().rev() { + let header = archive.get_header(&rwtxn, *block_hash)?; + let body = archive.get_body(&rwtxn, *block_hash)?; + let mainchain_blocks = { + let mut mainchain_blocks = Vec::new(); + if header.prev_main_hash != applied_main_frontier { + let mut reached_frontier = false; + while let Some(block_hash) = mainchain_block_batch.pop() { + mainchain_blocks.push(block_hash); + if block_hash == header.prev_main_hash { + reached_frontier = true; + break; + } + } + if !reached_frontier { + return Err(Error::InvalidMainchainInterval { + start: applied_main_frontier, + end: header.prev_main_hash, + }); } } - two_way_peg_data + mainchain_blocks }; - let () = match connect_tip_( + applied_main_frontier = header.prev_main_hash; + let () = match connect_tip_with_mainchain_blocks_( &mut rwtxn, archive, mempool, state, - header, - body, - &two_way_peg_data, + &header, + &body, + &mainchain_blocks, + accumulator, ) { Ok(()) => (), Err(err) => { - if is_fatal_reorg_error(&err) { + if matches!(err, Error::InvalidBlock(_)) { // The stored body for this block failed validation (e.g. a peer // supplied a body whose contents do not match the header's merkle // root). Abort the reorg and discard the invalid body from the @@ -423,28 +1033,16 @@ fn reorg_to_tip( // body is re-requested, instead of the archive staying poisoned. drop(rwtxn); let mut rwtxn = env.write_txn()?; - let () = - archive.delete_body(&mut rwtxn, header.hash(), body)?; + let () = archive.delete_body( + &mut rwtxn, + header.hash(), + &body, + )?; rwtxn.commit()?; } return Err(err); } }; - let new_tip_hash = state.try_get_tip(&rwtxn)?.unwrap(); - let bmm_verification = - archive.get_best_main_verification(&rwtxn, new_tip_hash)?; - let new_tip = Tip { - block_hash: new_tip_hash, - main_block_hash: bmm_verification, - }; - if let Some(tip) = tip - && archive.better_tip(&rwtxn, tip, new_tip)? != Some(new_tip) - { - continue; - } - rwtxn.commit().map_err(RwTxnError::from)?; - tracing::info!("synced to tip: {}", new_tip.block_hash); - rwtxn = env.write_txn().map_err(EnvError::from)?; } let tip = state.try_get_tip(&rwtxn)?; assert_eq!(tip, Some(new_tip.block_hash)); @@ -452,10 +1050,7 @@ fn reorg_to_tip( tracing::info!("synced to tip: {}", new_tip.block_hash); #[cfg(feature = "zmq")] { - for (idx, (header, _body)) in - blocks_to_apply.into_iter().rev().enumerate() - { - let block_hash = header.hash(); + for (idx, block_hash) in blocks_to_apply.into_iter().rev().enumerate() { let height = common_ancestor_height.map(|h| h + 1).unwrap_or(0) + idx as u32; let mut zmq_msg = zeromq::ZmqMessage::from("hashblock"); @@ -490,8 +1085,64 @@ struct NetTaskContext { type NewTipReadyMessage = (Tip, Option, Option>); +/// Tips whose atomic application is waiting for branch-independent withdrawal +/// bundle metadata. The optional address identifies the peer that supplied the +/// tip, if any, and is retained for the retry. +type MetadataBlockedTips = HashMap>>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MainchainConnectKind { + Bootstrap, + Next, + Stale, +} + +fn classify_mainchain_connect( + current_tip: Option, + block_hash: bitcoin::BlockHash, + prev_block_hash: bitcoin::BlockHash, +) -> MainchainConnectKind { + match current_tip { + None => MainchainConnectKind::Bootstrap, + Some(current_tip) if block_hash == current_tip => { + MainchainConnectKind::Stale + } + Some(current_tip) if prev_block_hash == current_tip => { + MainchainConnectKind::Next + } + Some(_) => MainchainConnectKind::Stale, + } +} + +fn block_tip_on_withdrawal_bundle_metadata( + ctxt: &NetTaskContext, + blocked_tips: &mut MetadataBlockedTips, + missing_withdrawal_bundles: &RwLock>, + tip: Tip, + source: Option, + event_block_hash: bitcoin::BlockHash, + m6id: M6id, +) { + blocked_tips + .entry(m6id) + .or_default() + .entry(tip) + .or_insert(source); + missing_withdrawal_bundles.write().insert(m6id); + let requested_from = ctxt.net.request_withdrawal_bundle(m6id, source); + tracing::warn!( + ?tip, + ?source, + %event_block_hash, + %m6id, + requested_from, + "atomic reorg blocked pending withdrawal bundle metadata" + ); +} + struct NetTask { ctxt: NetTaskContext, + missing_withdrawal_bundles: Arc>>, /// Receive a request to forward to the mainchain task, with the address of /// the peer connection that caused the request, and the peer state ID of /// the request @@ -503,6 +1154,7 @@ struct NetTask { forward_mainchain_task_request_tx: UnboundedSender<(mainchain_task::Request, SocketAddr, PeerStateId)>, mainchain_task_response_rx: UnboundedReceiver, + mainchain_event_rx: UnboundedReceiver, /// Receive a tip that is ready to reorg to, with the address of the peer /// connection that caused the request, if it originated from a peer. /// If the request originates from this node, then the socket address is @@ -521,6 +1173,7 @@ struct NetTask { } impl NetTask { + #[allow(clippy::too_many_arguments)] fn handle_response( ctxt: &NetTaskContext, // Attempt to switch to a descendant tip once a body has been @@ -530,7 +1183,10 @@ impl NetTask { crate::types::BlockHash, HashMap>, >, + canonical_main_tip: Option, new_tip_ready_tx: &UnboundedSender, + blocked_tips: &mut MetadataBlockedTips, + missing_withdrawal_bundles: &RwLock>, addr: SocketAddr, resp: PeerResponse, req: PeerRequest, @@ -604,46 +1260,52 @@ impl NetTask { .state .try_get_tip(&rotxn)? .map(|tip_hash| { - let bmm_verification = ctxt - .archive - .get_best_main_verification(&rotxn, tip_hash)?; - Ok::<_, Error>(Tip { - block_hash: tip_hash, - main_block_hash: bmm_verification, + tip_for_side_block( + &ctxt.archive, + &rotxn, + tip_hash, + canonical_main_tip, + )? + .ok_or_else(|| { + Error::Archive(archive::Error::NoBmmResult( + tip_hash, + )) }) }) .transpose()?; - // Find the BMM verification that is an ancestor of - // `main_descendant_tip` - let main_block_hash = ctxt - .archive - .get_bmm_results(&rotxn, block_hash)? - .into_iter() - .map(Result::<_, Error>::Ok) - .transpose_into_fallible() - .find_map(|(main_block_hash, bmm_result)| { - match bmm_result { - BmmResult::Failed => Ok(None), - BmmResult::Verified => { - if ctxt.archive.is_main_descendant( - &rotxn, - main_block_hash, - descendant_tip.main_block_hash, - )? { - Ok(Some(main_block_hash)) - } else { - Ok(None) - } - } - } - })? - .unwrap(); - let block_tip = Tip { - block_hash, - main_block_hash, + let descendant_tip = if canonical_main_tip.is_some() { + tip_for_side_block( + &ctxt.archive, + &rotxn, + descendant_tip.block_hash, + canonical_main_tip, + )? + } else { + Some(descendant_tip) + }; + let block_tip = if let Some(descendant_tip) = descendant_tip + { + // Find the newest verification of this block that is + // an ancestor of the advertised descendant's + // verification. + best_main_verification_on_lineage( + &ctxt.archive, + &rotxn, + block_hash, + descendant_tip.main_block_hash, + )? + .map(|main_block_hash| Tip { + block_hash, + main_block_hash, + }) + } else { + None }; - if header.prev_side_hash == tip.map(|tip| tip.block_hash) { + if let Some(block_tip) = block_tip + && header.prev_side_hash + == tip.map(|tip| tip.block_hash) + { tracing::trace!( ?block_tip, origin = %addr, @@ -659,6 +1321,20 @@ impl NetTask { return Ok(()); }; for (descendant_tip, sources) in block_descendant_tips { + let descendant_tip = if canonical_main_tip.is_some() { + let Some(descendant_tip) = tip_for_side_block( + &ctxt.archive, + &rotxn, + descendant_tip.block_hash, + canonical_main_tip, + )? + else { + continue; + }; + descendant_tip + } else { + descendant_tip + }; let common_ancestor_height = if let Some(tip) = tip && let Some(common_ancestor) = ctxt.archive.last_common_ancestor( @@ -699,17 +1375,15 @@ impl NetTask { .get_header(&rotxn, earliest_missing_body)? .prev_side_hash .map(|tip_hash| { - let bmm_verification = ctxt - .archive - .get_best_main_verification( - &rotxn, tip_hash, - )?; - Ok::<_, Error>(Tip { - block_hash: tip_hash, - main_block_hash: bmm_verification, - }) + tip_for_side_block( + &ctxt.archive, + &rotxn, + tip_hash, + canonical_main_tip, + ) }) .transpose()? + .flatten() } else { Some(descendant_tip) }; @@ -858,6 +1532,71 @@ impl NetTask { }), PeerResponse::NoHeader { block_hash }, ) if end == block_hash => Ok(()), + ( + PeerRequest::GetWithdrawalBundle( + peer_message::GetWithdrawalBundleRequest { + m6id: requested_m6id, + }, + ), + PeerResponse::WithdrawalBundle { + m6id: response_m6id, + metadata, + }, + ) => { + if requested_m6id != response_m6id + || metadata.compute_m6id() != requested_m6id + || !metadata.has_valid_inputs_commitment() + || !metadata.is_within_size_limit() + { + tracing::warn!( + %addr, + %requested_m6id, + %response_m6id, + "invalid withdrawal bundle metadata response" + ); + let () = ctxt.net.remove_active_peer(addr); + return Ok(()); + } + let mut rwtxn = ctxt.env.write_txn().map_err(EnvError::from)?; + ctxt.state + .put_withdrawal_bundle_metadata(&mut rwtxn, &metadata)?; + rwtxn.commit().map_err(RwTxnError::from)?; + + let retry_tips = blocked_tips.remove(&requested_m6id); + missing_withdrawal_bundles.write().remove(&requested_m6id); + let retry_count = retry_tips.as_ref().map_or(0, HashMap::len); + tracing::info!( + %requested_m6id, + %addr, + retry_count, + "recovered withdrawal bundle metadata" + ); + if let Some(retry_tips) = retry_tips { + for (tip, source) in retry_tips { + new_tip_ready_tx + .unbounded_send((tip, source, None)) + .map_err(Error::SendNewTipReady)?; + } + } + Ok(()) + } + ( + PeerRequest::GetWithdrawalBundle( + peer_message::GetWithdrawalBundleRequest { + m6id: requested_m6id, + }, + ), + PeerResponse::NoWithdrawalBundle { + m6id: response_m6id, + }, + ) if requested_m6id == response_m6id => { + tracing::trace!( + %addr, + %requested_m6id, + "peer does not have withdrawal bundle metadata" + ); + Ok(()) + } ( PeerRequest::PushTransaction( peer_message::PushTransactionRequest { transaction: _ }, @@ -873,6 +1612,7 @@ impl NetTask { ( req @ (PeerRequest::GetBlock { .. } | PeerRequest::GetHeaders { .. } + | PeerRequest::GetWithdrawalBundle { .. } | PeerRequest::PushTransaction { .. }), resp, ) => { @@ -902,12 +1642,14 @@ impl NetTask { PeerStateId, ), MainchainTaskResponse(mainchain_task::Response), + MainchainEvent(Option), // Apply new tip from peer or self. // An optional oneshot sender can be used receive the result of // attempting to reorg to the new tip, on the corresponding oneshot // receiver. NewTipReady(Tip, Option, Option>), PeerInfo(Option<(SocketAddr, Option)>), + RetryMissingWithdrawalBundles, // Signal to reconnect to a peer ReconnectPeer(SocketAddr), } @@ -953,6 +1695,9 @@ impl NetTask { let mainchain_task_response_stream = self .mainchain_task_response_rx .map(MailboxItem::MainchainTaskResponse); + let mainchain_event_stream = + StreamNotifyClose::new(self.mainchain_event_rx) + .map(MailboxItem::MainchainEvent); let new_tip_ready_stream = self.new_tip_ready_rx.map(|(block_hash, addr, resp_tx)| { MailboxItem::NewTipReady(block_hash, addr, resp_tx) @@ -962,13 +1707,22 @@ impl NetTask { let (reconnect_peer_spawner, reconnect_peer_rx) = join_set::new(); let reconnect_peer_stream = reconnect_peer_rx .map(|addr| MailboxItem::ReconnectPeer(addr.unwrap())); + let mut metadata_retry_interval = + tokio::time::interval(WITHDRAWAL_METADATA_RETRY_INTERVAL); + metadata_retry_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let metadata_retry_stream = + IntervalStream::new(metadata_retry_interval) + .map(|_| MailboxItem::RetryMissingWithdrawalBundles); let mut mailbox_stream = stream::select_all([ accept_connections.boxed(), forward_request_stream.boxed(), mainchain_task_response_stream.boxed(), + mainchain_event_stream.boxed(), new_tip_ready_stream.boxed(), peer_info_stream.boxed(), reconnect_peer_stream.boxed(), + metadata_retry_stream.boxed(), ]); // Attempt to switch to a descendant tip once a body has been // stored, if all other ancestor bodies are available. @@ -977,12 +1731,15 @@ impl NetTask { crate::types::BlockHash, HashMap>, >::new(); + let mut metadata_blocked_tips = MetadataBlockedTips::new(); // Map associating mainchain task requests with the peer(s) that // caused the request, and the request peer state ID let mut mainchain_task_request_sources = HashMap::< mainchain_task::Request, HashSet<(SocketAddr, PeerStateId)>, >::new(); + let mut canonical_main_tip = None; + let mut deferred_tip_ready = Vec::::new(); while let Some(mailbox_item) = mailbox_stream.next().await { tracing::trace!(?mailbox_item, "received new mailbox item"); match mailbox_item { @@ -994,6 +1751,16 @@ impl NetTask { } Ok(Some(addr)) => { tracing::trace!(%addr, "accepted new incoming connection"); + for m6id in self + .missing_withdrawal_bundles + .read() + .iter() + .copied() + { + self.ctxt + .net + .request_withdrawal_bundle(m6id, Some(addr)); + } } Err(fatal_err) => { // explicitly type error @@ -1052,10 +1819,289 @@ impl NetTask { } } } - } + } + MailboxItem::MainchainEvent(None) => { + return Err(Error::MainchainEventRxClosed); + } + MailboxItem::MainchainEvent(Some(Err(err))) => { + return Err(Error::MainchainEvent(Box::new(err))); + } + MailboxItem::MainchainEvent(Some(Ok(event))) => match event { + mainchain::Event::ConnectBlock { + header_info, + block_info, + } => { + let main_tip = header_info.block_hash; + let connect_kind = classify_mainchain_connect( + canonical_main_tip, + main_tip, + header_info.prev_block_hash, + ); + if connect_kind == MainchainConnectKind::Stale { + tracing::trace!( + %main_tip, + ?canonical_main_tip, + "ignoring stale mainchain connect after initial tip reconciliation" + ); + continue; + } + let mut attempted_tip = None; + let reorg_result = task::block_in_place(|| { + let rotxn = self + .ctxt + .env + .read_txn() + .map_err(EnvError::from)?; + let candidate = if connect_kind + == MainchainConnectKind::Bootstrap + { + best_ready_mainchain_candidate( + &self.ctxt.archive, + &self.ctxt.state, + &rotxn, + main_tip, + )? + } else if let Some(side_block) = + block_info.bmm_commitment + { + ready_mainchain_candidate( + &self.ctxt.archive, + &self.ctxt.state, + &rotxn, + side_block, + main_tip, + )? + } else { + None + }; + drop(rotxn); + if let Some(candidate) = candidate { + attempted_tip = Some(candidate); + reorg_to_tip_on_mainchain( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + #[cfg(feature = "zmq")] + &self.ctxt.zmq_pub_handler, + candidate, + main_tip, + ) + } else { + disconnect_for_mainchain_tip( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + main_tip, + ) + } + }); + match reorg_result { + Ok(_) => {} + Err(err) => match reorg_error_class(&err) { + ReorgErrorClass::Blocked => { + let Error::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + } = err + else { + unreachable!() + }; + let tip = attempted_tip.expect( + "metadata can only block candidate application", + ); + block_tip_on_withdrawal_bundle_metadata( + &self.ctxt, + &mut metadata_blocked_tips, + &self.missing_withdrawal_bundles, + tip, + None, + event_block_hash, + m6id, + ); + } + ReorgErrorClass::InvalidBlock + | ReorgErrorClass::Rejected => { + tracing::warn!( + %main_tip, + err = format!( + "{:#}", + ErrorChain::new(&err) + ), + "sidechain candidate rejected while processing mainchain event" + ); + task::block_in_place(|| { + disconnect_for_mainchain_tip( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + main_tip, + ) + })?; + } + ReorgErrorClass::Fatal => return Err(err), + }, + } + canonical_main_tip = Some(main_tip); + self.ctxt.net.set_canonical_main_tip(main_tip); + if connect_kind == MainchainConnectKind::Bootstrap { + for message in deferred_tip_ready.drain(..) { + self.new_tip_ready_tx + .unbounded_send(message) + .map_err(Error::SendNewTipReady)?; + } + } + } + mainchain::Event::DisconnectBlock { block_hash } => { + if canonical_main_tip + .is_some_and(|main_tip| main_tip != block_hash) + { + tracing::trace!( + %block_hash, + ?canonical_main_tip, + "ignoring stale mainchain disconnect after initial tip reconciliation" + ); + continue; + } + let main_tip = { + let rotxn = self + .ctxt + .env + .read_txn() + .map_err(EnvError::from)?; + self.ctxt + .archive + .try_get_main_header_info(&rotxn, &block_hash)? + .ok_or(archive::Error::NoMainHeaderInfo( + block_hash, + ))? + .prev_block_hash + }; + let mut attempted_tip = None; + let disconnect_result = task::block_in_place(|| { + let rotxn = self + .ctxt + .env + .read_txn() + .map_err(EnvError::from)?; + let candidate = best_ready_mainchain_candidate( + &self.ctxt.archive, + &self.ctxt.state, + &rotxn, + main_tip, + )?; + drop(rotxn); + if let Some(candidate) = candidate { + attempted_tip = Some(candidate); + reorg_to_tip_on_mainchain( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + #[cfg(feature = "zmq")] + &self.ctxt.zmq_pub_handler, + candidate, + main_tip, + ) + } else { + disconnect_for_mainchain_tip( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + main_tip, + ) + } + }); + if let Err(err) = disconnect_result { + match reorg_error_class(&err) { + ReorgErrorClass::Blocked => { + let Error::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + } = err + else { + unreachable!() + }; + let tip = attempted_tip.expect( + "metadata can only block candidate application", + ); + block_tip_on_withdrawal_bundle_metadata( + &self.ctxt, + &mut metadata_blocked_tips, + &self.missing_withdrawal_bundles, + tip, + None, + event_block_hash, + m6id, + ); + } + ReorgErrorClass::InvalidBlock + | ReorgErrorClass::Rejected => { + tracing::warn!( + %block_hash, + %main_tip, + err = format!( + "{:#}", + ErrorChain::new(&err) + ), + "sidechain rewind rejected while processing mainchain disconnect" + ); + if attempted_tip.is_some() { + let fallback_result = + task::block_in_place(|| { + disconnect_for_mainchain_tip( + &self.ctxt.env, + &self.ctxt.archive, + &self.ctxt.mempool, + &self.ctxt.state, + main_tip, + ) + }); + if let Err(fallback_err) = + fallback_result + { + match reorg_error_class( + &fallback_err, + ) { + ReorgErrorClass::Fatal + | ReorgErrorClass::Blocked => { + return Err(fallback_err); + } + ReorgErrorClass::InvalidBlock + | ReorgErrorClass::Rejected => { + tracing::warn!( + %block_hash, + %main_tip, + err = format!( + "{:#}", + ErrorChain::new( + &fallback_err + ) + ), + "fallback sidechain rewind rejected while processing mainchain disconnect" + ); + } + } + } + } + } + ReorgErrorClass::Fatal => return Err(err), + } + } + canonical_main_tip = Some(main_tip); + self.ctxt.net.set_canonical_main_tip(main_tip); + } + }, MailboxItem::NewTipReady(new_tip, addr, resp_tx) => { + let Some(main_tip) = canonical_main_tip else { + deferred_tip_ready.push((new_tip, addr, resp_tx)); + continue; + }; let reorg_result = task::block_in_place(|| { - reorg_to_tip( + reorg_to_tip_on_mainchain( &self.ctxt.env, &self.ctxt.archive, &self.ctxt.mempool, @@ -1063,24 +2109,60 @@ impl NetTask { #[cfg(feature = "zmq")] &self.ctxt.zmq_pub_handler, new_tip, + main_tip, ) }); let reorg_applied = match reorg_result { Ok(applied) => applied, - Err(err) if is_fatal_reorg_error(&err) => { - return Err(err); - } - // an invalid block must not kill the net task; drop the - // peer and keep running Err(err) => { - tracing::warn!( - ?new_tip, - ?addr, - err = format!("{:#}", ErrorChain::new(&err)), - "rejecting invalid tip from peer" - ); - if let Some(addr) = addr { - let () = self.ctxt.net.remove_active_peer(addr); + match reorg_error_class(&err) { + ReorgErrorClass::Blocked => { + let Error::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + } = err + else { + unreachable!() + }; + block_tip_on_withdrawal_bundle_metadata( + &self.ctxt, + &mut metadata_blocked_tips, + &self.missing_withdrawal_bundles, + new_tip, + addr, + event_block_hash, + m6id, + ); + } + ReorgErrorClass::InvalidBlock => { + tracing::warn!( + ?new_tip, + ?addr, + err = format!( + "{:#}", + ErrorChain::new(&err) + ), + "rejecting invalid tip from peer" + ); + if let Some(addr) = addr { + let () = self + .ctxt + .net + .remove_active_peer(addr); + } + } + ReorgErrorClass::Rejected => { + tracing::warn!( + ?new_tip, + ?addr, + err = format!( + "{:#}", + ErrorChain::new(&err) + ), + "tip rejected by current state" + ); + } + ReorgErrorClass::Fatal => return Err(err), } false } @@ -1186,7 +2268,10 @@ impl NetTask { Self::handle_response( &self.ctxt, &mut descendant_tips, + canonical_main_tip, &self.new_tip_ready_tx, + &mut metadata_blocked_tips, + &self.missing_withdrawal_bundles, addr, resp, req, @@ -1195,13 +2280,36 @@ impl NetTask { } } } + MailboxItem::RetryMissingWithdrawalBundles => { + let missing: Vec<_> = self + .missing_withdrawal_bundles + .read() + .iter() + .copied() + .collect(); + for m6id in missing { + self.ctxt.net.request_withdrawal_bundle(m6id, None); + } + } MailboxItem::ReconnectPeer(peer_address) => { match self .ctxt .net .connect_peer(self.ctxt.env.clone(), peer_address) { - Ok(()) => (), + Ok(()) => { + for m6id in self + .missing_withdrawal_bundles + .read() + .iter() + .copied() + { + self.ctxt.net.request_withdrawal_bundle( + m6id, + Some(peer_address), + ); + } + } Err(err) => { let err = anyhow::Error::from(err); tracing::error!( @@ -1222,6 +2330,7 @@ impl NetTask { #[derive(Clone)] pub(super) struct NetTaskHandle { task: Arc>, + missing_withdrawal_bundles: Arc>>, /// Push a tip that is ready to reorg to, with the address of the peer /// connection that caused the request, if it originated from a peer. /// If the request originates from this node, then the socket address is @@ -1239,6 +2348,7 @@ impl NetTaskHandle { archive: Archive, mainchain_task: MainchainTaskHandle, mainchain_task_response_rx: UnboundedReceiver, + mainchain_event_rx: UnboundedReceiver, mempool: MemPool, net: Net, peer_info_rx: PeerInfoRx, @@ -1260,11 +2370,14 @@ impl NetTaskHandle { forward_mainchain_task_request_rx, ) = mpsc::unbounded(); let (new_tip_ready_tx, new_tip_ready_rx) = mpsc::unbounded(); + let missing_withdrawal_bundles = Arc::new(RwLock::new(HashSet::new())); let task = NetTask { ctxt, + missing_withdrawal_bundles: Arc::clone(&missing_withdrawal_bundles), forward_mainchain_task_request_tx, forward_mainchain_task_request_rx, mainchain_task_response_rx, + mainchain_event_rx, new_tip_ready_tx: new_tip_ready_tx.clone(), new_tip_ready_rx, peer_info_rx, @@ -1277,10 +2390,15 @@ impl NetTaskHandle { }); NetTaskHandle { task: Arc::new(task), + missing_withdrawal_bundles, new_tip_ready_tx, } } + pub fn missing_withdrawal_bundles(&self) -> HashSet { + self.missing_withdrawal_bundles.read().clone() + } + /// Push a tip that is ready to reorg to, and await successful application. /// A result of Ok(true) indicates that the tip was applied and reorged /// to successfully. @@ -1313,21 +2431,1309 @@ impl Drop for NetTaskHandle { #[cfg(test)] mod test { + use bitcoin::hashes::Hash as _; + + #[cfg(feature = "zmq")] + use super::ZmqPubHandler; + use super::{ + Error, MainchainConnectKind, ReorgErrorClass, + accumulator_common_is_retained, accumulator_reorg_is_within_horizon, + best_ready_mainchain_candidate, classify_mainchain_connect, + classify_mainchain_reconciliation_error, connect_tip_, + disconnect_for_mainchain_tip, disconnect_tip_, is_fatal_reorg_error, + mainchain_interval, ready_mainchain_candidate, reorg_error_class, + reorg_to_tip_on_mainchain, with_accumulator_recovery, + }; use crate::{ - node::net_task::{Error, is_fatal_reorg_error}, - state, + archive::Archive, + mempool::MemPool, + state::{self, State}, + types::{ + Accumulator, AccumulatorDiff, Address, BitcoinOutputContent, Body, + FilledOutput, Header, M6id, OutPoint, Output, OutputContent, Tip, + WithdrawalBundleEvent, WithdrawalBundleEventStatus, + WithdrawalOutputContent, proto::mainchain, utreexo_leaf_hash, + }, }; + fn temp_env( + test_name: &str, + ) -> anyhow::Result<(temp_dir::TempDir, sneed::Env)> { + let temp_dir = temp_dir::TempDir::with_prefix(format!( + "plain-bitassets-{test_name}-{}", + std::process::id() + ))?; + let mut opts = heed::EnvOpenOptions::new(); + opts.map_size(64 * 1024 * 1024) + .max_dbs(State::NUM_DBS + Archive::NUM_DBS + MemPool::NUM_DBS); + let env = unsafe { sneed::Env::open(&opts, temp_dir.path()) }?; + Ok((temp_dir, env)) + } + + #[test] + fn connect_tip_persists_and_applies_accumulator_diffs() -> anyhow::Result<()> + { + let (_temp_dir, env) = + temp_env("connect_tip_persists_and_applies_accumulator_diffs")?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Accumulator::default())?; + let mut accumulator = Accumulator::default(); + let mempool = MemPool::new(&env)?; + let coinbase = Output::new( + Address::ALL_ZEROS, + OutputContent::Bitcoin(BitcoinOutputContent(bitcoin::Amount::ZERO)), + ); + let body = Body { + coinbase: vec![coinbase], + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let header = Header { + merkle_root: Body::compute_merkle_root( + &body.coinbase, + &body.transactions, + ), + prev_side_hash: None, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + }; + let block_hash = header.hash(); + let outpoint = OutPoint::Coinbase { + merkle_root: header.merkle_root, + vout: 0, + }; + let output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::ZERO, + ); + let mut expected_diff = AccumulatorDiff::default(); + expected_diff.insert(utreexo_leaf_hash(&outpoint, &output)); + let deposit_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([2; 32]), + vout: 0, + }; + let deposit_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(100), + ); + expected_diff.insert(utreexo_leaf_hash( + &OutPoint::Deposit(deposit_outpoint), + &deposit_output, + )); + let mut expected = Accumulator::default(); + expected.apply_diff(expected_diff.clone())?; + let mut two_way_peg_data = mainchain::TwoWayPegData::default(); + two_way_peg_data.block_info.insert( + header.prev_main_hash, + mainchain::BlockInfo { + bmm_commitment: None, + events: vec![mainchain::BlockEvent::Deposit( + mainchain::Deposit { + tx_index: 0, + outpoint: deposit_outpoint, + output: deposit_output, + }, + )], + }, + ); + + let mut rwtxn = env.write_txn()?; + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + &header, + &body, + &two_way_peg_data, + &mut accumulator, + )?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + assert_eq!( + archive.get_accumulator_diff(&rotxn, block_hash)?, + expected_diff + ); + assert_eq!(accumulator.get_roots(), expected.get_roots()); + + drop(rotxn); + let empty_body = Body { + coinbase: Vec::new(), + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let child_header = Header { + merkle_root: Body::compute_merkle_root( + &empty_body.coinbase, + &empty_body.transactions, + ), + prev_side_hash: Some(block_hash), + prev_main_hash: header.prev_main_hash, + }; + let child_hash = child_header.hash(); + let mut rwtxn = env.write_txn()?; + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + &child_header, + &empty_body, + &mainchain::TwoWayPegData::default(), + &mut accumulator, + )?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + assert!(archive.get_accumulator_diff(&rotxn, child_hash)?.is_empty()); + assert_eq!(accumulator.get_roots(), expected.get_roots()); + Ok(()) + } + + #[test] + fn mainchain_interval_rejects_unrelated_boundary() -> anyhow::Result<()> { + let (_temp_dir, env) = + temp_env("mainchain_interval_rejects_unrelated_boundary")?; + let archive = Archive::new(&env)?; + let main_a = bitcoin::BlockHash::from_byte_array([31; 32]); + let main_b = bitcoin::BlockHash::from_byte_array([32; 32]); + let mut rwtxn = env.write_txn()?; + for (block_hash, work) in [(main_a, 1), (main_b, 2)] { + archive.put_main_header_info( + &mut rwtxn, + &mainchain::BlockHeaderInfo { + block_hash, + prev_block_hash: bitcoin::BlockHash::all_zeros(), + height: 1, + work: bitcoin::Work::from_le_bytes([work; 32]), + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + block_hash, + &mainchain::BlockInfo::default(), + )?; + } + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + assert!(matches!( + mainchain_interval(&archive, &rotxn, Some(main_a), main_b), + Err(Error::InvalidMainchainInterval { start, end }) + if start == main_a && end == main_b + )); + Ok(()) + } + + #[test] + fn mainchain_connects_are_sequential_after_bootstrap() { + let current = bitcoin::BlockHash::from_byte_array([1; 32]); + let child = bitcoin::BlockHash::from_byte_array([2; 32]); + let sibling = bitcoin::BlockHash::from_byte_array([3; 32]); + let parent = bitcoin::BlockHash::from_byte_array([4; 32]); + + assert_eq!( + classify_mainchain_connect(None, current, parent), + MainchainConnectKind::Bootstrap + ); + assert_eq!( + classify_mainchain_connect(Some(current), current, parent), + MainchainConnectKind::Stale + ); + assert_eq!( + classify_mainchain_connect(Some(current), child, current), + MainchainConnectKind::Next + ); + assert_eq!( + classify_mainchain_connect(Some(current), sibling, parent), + MainchainConnectKind::Stale + ); + } + + #[test] + fn startup_candidate_uses_full_canonical_mainchain_lineage() + -> anyhow::Result<()> { + let (_temp_dir, env) = + temp_env("startup_candidate_uses_full_mainchain_lineage")?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Accumulator::default())?; + let main1 = bitcoin::BlockHash::from_byte_array([11; 32]); + let main2 = bitcoin::BlockHash::from_byte_array([12; 32]); + let fork2 = bitcoin::BlockHash::from_byte_array([13; 32]); + let body = Body { + coinbase: Vec::new(), + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let canonical_header = Header { + merkle_root: Body::compute_merkle_root( + &body.coinbase, + &body.transactions, + ), + prev_side_hash: None, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + }; + let canonical_side = canonical_header.hash(); + let fork_body = Body { + coinbase: vec![Output { + address: Address::ALL_ZEROS, + content: OutputContent::Bitcoin(BitcoinOutputContent( + bitcoin::Amount::ZERO, + )), + memo: vec![1], + }], + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let fork_header = Header { + merkle_root: Body::compute_merkle_root( + &fork_body.coinbase, + &fork_body.transactions, + ), + prev_side_hash: None, + prev_main_hash: main1, + }; + let fork_side = fork_header.hash(); + + let mut rwtxn = env.write_txn()?; + for header_info in [ + mainchain::BlockHeaderInfo { + block_hash: main1, + prev_block_hash: bitcoin::BlockHash::all_zeros(), + height: 1, + work: bitcoin::Work::from_le_bytes([1; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main2, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([1; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: fork2, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([2; 32]), + }, + ] { + archive.put_main_header_info(&mut rwtxn, &header_info)?; + } + for (main_hash, commitment) in [ + (main1, Some(canonical_side)), + (main2, None), + (fork2, Some(fork_side)), + ] { + archive.put_main_block_info( + &mut rwtxn, + main_hash, + &mainchain::BlockInfo { + bmm_commitment: commitment, + events: Vec::new(), + }, + )?; + } + archive.put_header(&mut rwtxn, &canonical_header)?; + archive.put_body(&mut rwtxn, canonical_side, &body)?; + archive.put_header(&mut rwtxn, &fork_header)?; + archive.put_body(&mut rwtxn, fork_side, &fork_body)?; + rwtxn.commit()?; + + let rotxn = env.read_txn()?; + assert_eq!( + archive.get_mainchain_verified_tips(&rotxn, main2)?, + vec![Tip { + block_hash: canonical_side, + main_block_hash: main1, + }] + ); + assert_eq!( + best_ready_mainchain_candidate(&archive, &state, &rotxn, main2)?, + Some(Tip { + block_hash: canonical_side, + main_block_hash: main1, + }) + ); + Ok(()) + } + + #[test] + fn cumulative_reorg_cannot_cross_retained_floor() { + assert!(accumulator_common_is_retained(Some(100), 100)); + assert!(accumulator_common_is_retained(Some(101), 100)); + assert!(!accumulator_common_is_retained(Some(99), 100)); + assert!(accumulator_common_is_retained(None, 0)); + assert!(!accumulator_common_is_retained(None, 1)); + } + + #[test] + fn reorg_horizon_is_inclusive() { + let horizon = + u64::from(crate::archive::ACCUMULATOR_REORG_HORIZON_BLOCKS); + assert!(accumulator_reorg_is_within_horizon(horizon)); + assert!(!accumulator_reorg_is_within_horizon(horizon + 1)); + } + + #[test] + fn panicked_reorg_restores_committed_accumulator() -> anyhow::Result<()> { + let (_temp_dir, env) = + temp_env("panicked_reorg_restores_committed_accumulator")?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Accumulator::default())?; + let mut accumulator = Accumulator::default(); + let mut initial_diff = AccumulatorDiff::default(); + initial_diff.insert([1; 32]); + accumulator.apply_diff(initial_diff)?; + + let panic = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + drop(with_accumulator_recovery( + &env, + &archive, + &state, + &mut accumulator, + |accumulator| -> Result<(), Error> { + let mut uncommitted_diff = AccumulatorDiff::default(); + uncommitted_diff.insert([2; 32]); + accumulator.apply_diff(uncommitted_diff)?; + panic!("injected reorg panic"); + }, + )); + })); + + assert!(panic.is_err()); + assert_eq!( + bincode::serialize(&accumulator)?, + bincode::serialize(&Accumulator::default())? + ); + Ok(()) + } + + #[test] + fn l2_reorg_replays_finalized_withdrawal_and_later_deposit() + -> anyhow::Result<()> { + let (_temp_dir, env) = temp_env( + "l2_reorg_replays_finalized_withdrawal_and_later_deposit", + )?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Accumulator::default())?; + let mempool = MemPool::new(&env)?; + #[cfg(feature = "zmq")] + let zmq_runtime = tokio::runtime::Runtime::new()?; + #[cfg(feature = "zmq")] + let (zmq_tx, _zmq_rx) = futures::channel::mpsc::unbounded(); + #[cfg(feature = "zmq")] + let zmq_pub_handler = ZmqPubHandler { + tx: zmq_tx, + _handle: zmq_runtime.spawn(async {}), + }; + + let main1 = bitcoin::BlockHash::from_byte_array([101; 32]); + let main2 = bitcoin::BlockHash::from_byte_array([102; 32]); + let main3 = bitcoin::BlockHash::from_byte_array([103; 32]); + let main4 = bitcoin::BlockHash::from_byte_array([104; 32]); + let old_main5 = bitcoin::BlockHash::from_byte_array([105; 32]); + let new_main5 = bitcoin::BlockHash::from_byte_array([106; 32]); + let new_main6 = bitcoin::BlockHash::from_byte_array([107; 32]); + let new_main7 = bitcoin::BlockHash::from_byte_array([108; 32]); + let empty_body = Body { + coinbase: Vec::new(), + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let withdrawal_content = WithdrawalOutputContent { + value: bitcoin::Amount::ZERO, + main_fee: bitcoin::Amount::ZERO, + main_address: bitcoin::Address::p2pkh( + bitcoin::PubkeyHash::hash(b"reorg withdrawal"), + bitcoin::NetworkKind::Test, + ) + .into_unchecked(), + }; + let withdrawal_body = Body { + coinbase: vec![Output { + address: Address::ALL_ZEROS, + content: OutputContent::Withdrawal(withdrawal_content), + memo: vec![1], + }], + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let genesis = Header { + merkle_root: Body::compute_merkle_root( + &withdrawal_body.coinbase, + &withdrawal_body.transactions, + ), + prev_side_hash: None, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + }; + let common1 = Header { + merkle_root: Body::compute_merkle_root(&[], &[]), + prev_side_hash: Some(genesis.hash()), + prev_main_hash: main1, + }; + let common2 = Header { + merkle_root: common1.merkle_root, + prev_side_hash: Some(common1.hash()), + prev_main_hash: main2, + }; + let common3 = Header { + merkle_root: common1.merkle_root, + prev_side_hash: Some(common2.hash()), + prev_main_hash: main3, + }; + let old_tip_header = Header { + merkle_root: common1.merkle_root, + prev_side_hash: Some(common3.hash()), + prev_main_hash: main4, + }; + let replacement_header = Header { + merkle_root: common1.merkle_root, + prev_side_hash: Some(common3.hash()), + prev_main_hash: new_main6, + }; + let old_tip = old_tip_header.hash(); + let replacement_tip = replacement_header.hash(); + + let mut rwtxn = env.write_txn()?; + for (block_hash, prev_block_hash, height, work) in [ + (main1, bitcoin::BlockHash::all_zeros(), 1, 1_u8), + (main2, main1, 2, 2), + (main3, main2, 3, 3), + (main4, main3, 4, 4), + (old_main5, main4, 5, 5), + ] { + archive.put_main_header_info( + &mut rwtxn, + &mainchain::BlockHeaderInfo { + block_hash, + prev_block_hash, + height, + work: bitcoin::Work::from_le_bytes([work; 32]), + }, + )?; + } + for (block_hash, commitment) in [ + (main1, genesis.hash()), + (main2, common1.hash()), + (main3, common2.hash()), + (main4, common3.hash()), + (old_main5, old_tip), + ] { + archive.put_main_block_info( + &mut rwtxn, + block_hash, + &mainchain::BlockInfo { + bmm_commitment: Some(commitment), + events: Vec::new(), + }, + )?; + } + + let mut accumulator = Accumulator::default(); + for (header, body) in [ + (&genesis, &withdrawal_body), + (&common1, &empty_body), + (&common2, &empty_body), + (&common3, &empty_body), + (&old_tip_header, &empty_body), + ] { + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + header, + body, + &mainchain::TwoWayPegData::default(), + &mut accumulator, + )?; + } + let (bundle, creation_height) = state + .try_get_pending_withdrawal_bundle(&rwtxn)? + .expect("height-four old branch should create a bundle"); + assert_eq!(creation_height, 4); + let m6id = bundle.compute_m6id(); + assert!( + state + .try_get_withdrawal_bundle_metadata(&rwtxn, m6id)? + .is_some() + ); + rwtxn.commit()?; + *state.utreexo_accumulator.lock() = accumulator; + + let later_deposit_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([109; 32]), + vout: 0, + }; + let later_deposit_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(50), + ); + let mut rwtxn = env.write_txn()?; + for (block_hash, prev_block_hash, height, work) in [ + (new_main5, main4, 5, 6_u8), + (new_main6, new_main5, 6, 7), + (new_main7, new_main6, 7, 8), + ] { + archive.put_main_header_info( + &mut rwtxn, + &mainchain::BlockHeaderInfo { + block_hash, + prev_block_hash, + height, + work: bitcoin::Work::from_le_bytes([work; 32]), + }, + )?; + } + archive.put_main_block_info( + &mut rwtxn, + new_main5, + &mainchain::BlockInfo { + bmm_commitment: None, + events: vec![mainchain::BlockEvent::WithdrawalBundle( + WithdrawalBundleEvent { + m6id, + status: WithdrawalBundleEventStatus::Submitted, + }, + )], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + new_main6, + &mainchain::BlockInfo { + bmm_commitment: None, + events: vec![ + mainchain::BlockEvent::WithdrawalBundle( + WithdrawalBundleEvent { + m6id, + status: WithdrawalBundleEventStatus::Confirmed, + }, + ), + mainchain::BlockEvent::Deposit(mainchain::Deposit { + tx_index: 1, + outpoint: later_deposit_outpoint, + output: later_deposit_output.clone(), + }), + ], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + new_main7, + &mainchain::BlockInfo { + bmm_commitment: Some(replacement_tip), + events: Vec::new(), + }, + )?; + archive.put_header(&mut rwtxn, &replacement_header)?; + archive.put_body(&mut rwtxn, replacement_tip, &empty_body)?; + rwtxn.commit()?; + + assert!(reorg_to_tip_on_mainchain( + &env, + &archive, + &mempool, + &state, + #[cfg(feature = "zmq")] + &zmq_pub_handler, + Tip { + block_hash: replacement_tip, + main_block_hash: new_main7, + }, + new_main7, + )?); + + let rotxn = env.read_txn()?; + let withdrawal_outpoint = OutPoint::Coinbase { + merkle_root: genesis.merkle_root, + vout: 0, + }; + let utxos = state.get_utxos(&rotxn)?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(replacement_tip)); + assert!(!utxos.contains_key(&withdrawal_outpoint)); + assert_eq!( + utxos.get(&OutPoint::Deposit(later_deposit_outpoint)), + Some(&later_deposit_output) + ); + assert!(state.try_get_pending_withdrawal_bundle(&rotxn)?.is_none()); + assert!( + state + .try_get_withdrawal_bundle_metadata(&rotxn, m6id)? + .is_some() + ); + + let mut expected = Accumulator::default(); + let mut expected_diff = AccumulatorDiff::default(); + expected_diff.insert(utreexo_leaf_hash( + &OutPoint::Deposit(later_deposit_outpoint), + &later_deposit_output, + )); + expected.apply_diff(expected_diff)?; + assert_eq!( + state.utreexo_accumulator.lock().get_roots(), + expected.get_roots() + ); + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + bincode::serialize( + &archive.accumulator_at(&rotxn, Some(replacement_tip))? + )? + ); + Ok(()) + } + + #[test] + fn raw_mainchain_disconnect_rewinds_accumulator_and_reapplies_deposits() + -> anyhow::Result<()> { + let (_temp_dir, env) = + temp_env("raw_mainchain_disconnect_rewinds_accumulator")?; + let archive = Archive::new(&env)?; + let state = State::new(&env, Accumulator::default())?; + let mempool = MemPool::new(&env)?; + #[cfg(feature = "zmq")] + let zmq_runtime = tokio::runtime::Runtime::new()?; + #[cfg(feature = "zmq")] + let (zmq_tx, _zmq_rx) = futures::channel::mpsc::unbounded(); + #[cfg(feature = "zmq")] + let zmq_pub_handler = ZmqPubHandler { + tx: zmq_tx, + _handle: zmq_runtime.spawn(async {}), + }; + + let main1 = bitcoin::BlockHash::from_byte_array([11; 32]); + let main2a = bitcoin::BlockHash::from_byte_array([12; 32]); + let main2b = bitcoin::BlockHash::from_byte_array([13; 32]); + let main3a = bitcoin::BlockHash::from_byte_array([14; 32]); + let main3b = bitcoin::BlockHash::from_byte_array([15; 32]); + let abandoned_main3b = bitcoin::BlockHash::from_byte_array([16; 32]); + let empty_body = Body { + coinbase: Vec::new(), + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let genesis = Header { + merkle_root: Body::compute_merkle_root( + &empty_body.coinbase, + &empty_body.transactions, + ), + prev_side_hash: None, + prev_main_hash: bitcoin::BlockHash::all_zeros(), + }; + let genesis_hash = genesis.hash(); + let branch_body = |marker| Body { + coinbase: vec![Output { + address: Address::ALL_ZEROS, + content: OutputContent::Bitcoin(BitcoinOutputContent( + bitcoin::Amount::ZERO, + )), + memo: vec![marker], + }], + transactions: Vec::new(), + authorizations: Vec::new(), + }; + let body_a = branch_body(1); + let body_b = branch_body(2); + let header_a = Header { + merkle_root: Body::compute_merkle_root( + &body_a.coinbase, + &body_a.transactions, + ), + prev_side_hash: Some(genesis_hash), + prev_main_hash: main2a, + }; + let header_b = Header { + merkle_root: Body::compute_merkle_root( + &body_b.coinbase, + &body_b.transactions, + ), + prev_side_hash: Some(genesis_hash), + prev_main_hash: main2b, + }; + let hash_a = header_a.hash(); + let hash_b = header_b.hash(); + + let deposit_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([21; 32]), + vout: 0, + }; + let deposit_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(50), + ); + let deposit_event = + mainchain::BlockEvent::Deposit(mainchain::Deposit { + tx_index: 0, + outpoint: deposit_outpoint, + output: deposit_output.clone(), + }); + let deposit_a_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([22; 32]), + vout: 0, + }; + let deposit_a_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(60), + ); + let deposit_a_event = + mainchain::BlockEvent::Deposit(mainchain::Deposit { + tx_index: 0, + outpoint: deposit_a_outpoint, + output: deposit_a_output.clone(), + }); + let deposit_b_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([23; 32]), + vout: 0, + }; + let deposit_b_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(70), + ); + let deposit_b_event = + mainchain::BlockEvent::Deposit(mainchain::Deposit { + tx_index: 0, + outpoint: deposit_b_outpoint, + output: deposit_b_output.clone(), + }); + + let mut rwtxn = env.write_txn()?; + let main_headers = [ + mainchain::BlockHeaderInfo { + block_hash: main1, + prev_block_hash: bitcoin::BlockHash::all_zeros(), + height: 1, + work: bitcoin::Work::from_le_bytes([1; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main2a, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([1; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main2b, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([2; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main3a, + prev_block_hash: main2a, + height: 3, + work: bitcoin::Work::from_le_bytes([1; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main3b, + prev_block_hash: main2b, + height: 3, + work: bitcoin::Work::from_le_bytes([2; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: abandoned_main3b, + prev_block_hash: main2b, + height: 3, + work: bitcoin::Work::from_le_bytes([4; 32]), + }, + ]; + for header_info in &main_headers { + archive.put_main_header_info(&mut rwtxn, header_info)?; + } + archive.put_main_block_info( + &mut rwtxn, + main1, + &mainchain::BlockInfo { + bmm_commitment: Some(genesis_hash), + events: vec![deposit_event.clone()], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main2a, + &mainchain::BlockInfo { + bmm_commitment: None, + events: vec![deposit_a_event.clone()], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main2b, + &mainchain::BlockInfo { + bmm_commitment: None, + events: vec![deposit_b_event.clone()], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main3a, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_a), + events: Vec::new(), + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main3b, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_b), + events: Vec::new(), + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + abandoned_main3b, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_b), + events: Vec::new(), + }, + )?; + + let mut accumulator = Accumulator::default(); + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + &genesis, + &empty_body, + &mainchain::TwoWayPegData::default(), + &mut accumulator, + )?; + let mut peg_data = mainchain::TwoWayPegData::default(); + peg_data.block_info.insert( + main1, + mainchain::BlockInfo { + bmm_commitment: Some(genesis_hash), + events: vec![deposit_event], + }, + ); + peg_data.block_info.insert( + main2a, + mainchain::BlockInfo { + bmm_commitment: None, + events: vec![deposit_a_event], + }, + ); + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + &header_a, + &body_a, + &peg_data, + &mut accumulator, + )?; + let empty_child = Header { + merkle_root: Body::compute_merkle_root( + &empty_body.coinbase, + &empty_body.transactions, + ), + prev_side_hash: Some(hash_a), + prev_main_hash: main2a, + }; + connect_tip_( + &mut rwtxn, + &archive, + &mempool, + &state, + &empty_child, + &empty_body, + &mainchain::TwoWayPegData::default(), + &mut accumulator, + )?; + archive.put_header(&mut rwtxn, &header_b)?; + archive.put_body(&mut rwtxn, hash_b, &body_b)?; + rwtxn.commit()?; + + // Disconnecting an eventless child must not infer and undo the + // deposit interval already applied by its still-connected parent. + let mut rwtxn = env.write_txn()?; + disconnect_tip_(&mut rwtxn, &archive, &mempool, &state)?; + rwtxn.commit()?; + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(hash_a)); + assert!( + state + .get_utxos(&rotxn)? + .contains_key(&OutPoint::Deposit(deposit_outpoint)) + ); + assert!( + state + .get_utxos(&rotxn)? + .contains_key(&OutPoint::Deposit(deposit_a_outpoint)) + ); + *state.utreexo_accumulator.lock() = + archive.accumulator_at(&rotxn, Some(hash_a))?; + drop(rotxn); + + // Disconnecting the mainchain block that verified A immediately + // rewinds to the newest side block whose verification is still on the + // surviving mainchain. The operation commits the state and forest as + // one unit; the abandoned branch's deposits disappear with A. + assert!(disconnect_for_mainchain_tip( + &env, &archive, &mempool, &state, main2a, + )?); + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(genesis_hash)); + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + bincode::serialize( + &archive.accumulator_at(&rotxn, Some(genesis_hash))? + )? + ); + assert!( + !state + .get_utxos(&rotxn)? + .contains_key(&OutPoint::Deposit(deposit_outpoint)) + ); + assert!( + !state + .get_utxos(&rotxn)? + .contains_key(&OutPoint::Deposit(deposit_a_outpoint)) + ); + + // A connect event carrying B's commitment can now discover the fully + // archived candidate and apply it automatically on the new L1 branch. + let candidate = ready_mainchain_candidate( + &archive, &state, &rotxn, hash_b, main3b, + )? + .unwrap(); + assert_eq!( + archive.get_best_main_verification(&rotxn, hash_b)?, + abandoned_main3b + ); + assert_eq!(candidate.main_block_hash, main3b); + drop(rotxn); + let applied = reorg_to_tip_on_mainchain( + &env, + &archive, + &mempool, + &state, + #[cfg(feature = "zmq")] + &zmq_pub_handler, + candidate, + main3b, + )?; + assert!(applied); + + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(hash_b)); + assert_eq!( + state + .get_utxos(&rotxn)? + .get(&OutPoint::Deposit(deposit_outpoint)), + Some(&deposit_output) + ); + assert_eq!( + state + .get_utxos(&rotxn)? + .get(&OutPoint::Deposit(deposit_a_outpoint)), + None + ); + assert_eq!( + state + .get_utxos(&rotxn)? + .get(&OutPoint::Deposit(deposit_b_outpoint)), + Some(&deposit_b_output) + ); + + let mut expected = Accumulator::default(); + let coinbase_outpoint = OutPoint::Coinbase { + merkle_root: header_b.merkle_root, + vout: 0, + }; + let coinbase_output = FilledOutput { + address: body_b.coinbase[0].address, + content: crate::types::FilledOutputContent::Bitcoin( + BitcoinOutputContent(bitcoin::Amount::ZERO), + ), + memo: body_b.coinbase[0].memo.clone(), + }; + let mut expected_diff = AccumulatorDiff::default(); + expected_diff + .insert(utreexo_leaf_hash(&coinbase_outpoint, &coinbase_output)); + expected_diff.insert(utreexo_leaf_hash( + &OutPoint::Deposit(deposit_outpoint), + &deposit_output, + )); + expected_diff.insert(utreexo_leaf_hash( + &OutPoint::Deposit(deposit_b_outpoint), + &deposit_b_output, + )); + expected.apply_diff(expected_diff)?; + let expected_bytes = bincode::serialize(&expected)?; + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + expected_bytes.clone() + ); + + // Missing withdrawal metadata is a recoverable sync block. Even when + // the current sidechain tip has no verification on the replacement L1 + // lineage, neither its database state nor its accumulator is replaced + // until the complete candidate succeeds. + drop(rotxn); + let main2d = bitcoin::BlockHash::from_byte_array([41; 32]); + let main3d = bitcoin::BlockHash::from_byte_array([42; 32]); + let body_d = branch_body(6); + let header_d = Header { + merkle_root: Body::compute_merkle_root( + &body_d.coinbase, + &body_d.transactions, + ), + prev_side_hash: Some(genesis_hash), + prev_main_hash: main2d, + }; + let hash_d = header_d.hash(); + let missing_m6id = M6id(bitcoin::Txid::from_byte_array([43; 32])); + let mut rwtxn = env.write_txn()?; + for header_info in [ + mainchain::BlockHeaderInfo { + block_hash: main2d, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([5; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main3d, + prev_block_hash: main2d, + height: 3, + work: bitcoin::Work::from_le_bytes([5; 32]), + }, + ] { + archive.put_main_header_info(&mut rwtxn, &header_info)?; + } + archive.put_main_block_info( + &mut rwtxn, + main2d, + &mainchain::BlockInfo { + bmm_commitment: None, + events: vec![mainchain::BlockEvent::WithdrawalBundle( + WithdrawalBundleEvent { + m6id: missing_m6id, + status: WithdrawalBundleEventStatus::Submitted, + }, + )], + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main3d, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_d), + events: Vec::new(), + }, + )?; + archive.put_header(&mut rwtxn, &header_d)?; + archive.put_body(&mut rwtxn, hash_d, &body_d)?; + rwtxn.commit()?; + + let err = reorg_to_tip_on_mainchain( + &env, + &archive, + &mempool, + &state, + #[cfg(feature = "zmq")] + &zmq_pub_handler, + Tip { + block_hash: hash_d, + main_block_hash: main3d, + }, + main3d, + ) + .unwrap_err(); + assert!(matches!( + err, + Error::MissingWithdrawalBundleMetadata { m6id, .. } + if m6id == missing_m6id + )); + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(hash_b)); + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + expected_bytes + ); + + // A later invalid block rolls back the entire replacement branch, not + // just the invalid block. The in-memory forest is restored to the last + // committed tip, and the poisoned body is removed from the archive. + drop(rotxn); + let main2c = bitcoin::BlockHash::from_byte_array([31; 32]); + let main3c = bitcoin::BlockHash::from_byte_array([32; 32]); + let main4c = bitcoin::BlockHash::from_byte_array([33; 32]); + let body_c1 = branch_body(3); + let header_c1 = Header { + merkle_root: Body::compute_merkle_root( + &body_c1.coinbase, + &body_c1.transactions, + ), + prev_side_hash: Some(genesis_hash), + prev_main_hash: main2c, + }; + let hash_c1 = header_c1.hash(); + let header_body_c2 = branch_body(4); + let invalid_body_c2 = branch_body(5); + let header_c2 = Header { + merkle_root: Body::compute_merkle_root( + &header_body_c2.coinbase, + &header_body_c2.transactions, + ), + prev_side_hash: Some(hash_c1), + prev_main_hash: main3c, + }; + let hash_c2 = header_c2.hash(); + let mut rwtxn = env.write_txn()?; + for header_info in [ + mainchain::BlockHeaderInfo { + block_hash: main2c, + prev_block_hash: main1, + height: 2, + work: bitcoin::Work::from_le_bytes([3; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main3c, + prev_block_hash: main2c, + height: 3, + work: bitcoin::Work::from_le_bytes([3; 32]), + }, + mainchain::BlockHeaderInfo { + block_hash: main4c, + prev_block_hash: main3c, + height: 4, + work: bitcoin::Work::from_le_bytes([3; 32]), + }, + ] { + archive.put_main_header_info(&mut rwtxn, &header_info)?; + } + archive.put_main_block_info( + &mut rwtxn, + main2c, + &mainchain::BlockInfo::default(), + )?; + archive.put_main_block_info( + &mut rwtxn, + main3c, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_c1), + events: Vec::new(), + }, + )?; + archive.put_main_block_info( + &mut rwtxn, + main4c, + &mainchain::BlockInfo { + bmm_commitment: Some(hash_c2), + events: Vec::new(), + }, + )?; + archive.put_header(&mut rwtxn, &header_c1)?; + archive.put_body(&mut rwtxn, hash_c1, &body_c1)?; + archive.put_header(&mut rwtxn, &header_c2)?; + archive.put_body(&mut rwtxn, hash_c2, &invalid_body_c2)?; + rwtxn.commit()?; + + let err = reorg_to_tip_on_mainchain( + &env, + &archive, + &mempool, + &state, + #[cfg(feature = "zmq")] + &zmq_pub_handler, + Tip { + block_hash: hash_c2, + main_block_hash: main4c, + }, + main4c, + ) + .unwrap_err(); + assert!(matches!(err, Error::InvalidBlock(_))); + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(hash_b)); + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + expected_bytes + ); + assert!(archive.try_get_accumulator_diff(&rotxn, hash_c1)?.is_none()); + assert!(archive.try_get_body(&rotxn, hash_c1)?.is_some()); + assert!(archive.try_get_body(&rotxn, hash_c2)?.is_none()); + + // A current tip without verification on the canonical mainchain cannot + // silently keep running when the required replacement crosses the + // retained accumulator floor. + drop(rotxn); + let mut rwtxn = env.write_txn()?; + archive.advance_accumulator_reorg_floor(&mut rwtxn, 1)?; + rwtxn.commit()?; + let err = reorg_to_tip_on_mainchain( + &env, + &archive, + &mempool, + &state, + #[cfg(feature = "zmq")] + &zmq_pub_handler, + Tip { + block_hash: hash_c2, + main_block_hash: main4c, + }, + main4c, + ) + .unwrap_err(); + assert!(matches!( + err, + Error::MainchainDisconnectBelowAccumulatorFloor { + target_height: Some(0), + retained_floor: 1, + } + )); + let rotxn = env.read_txn()?; + assert_eq!(state.try_get_tip(&rotxn)?, Some(hash_b)); + assert_eq!( + bincode::serialize(&*state.utreexo_accumulator.lock())?, + expected_bytes + ); + Ok(()) + } + // a peer's invalid block (value out > value in) must not be fatal #[test] fn invalid_peer_block_is_not_fatal() { + let err = Error::InvalidBlock(Box::new(state::Error::NotEnoughFees)); + assert!(!is_fatal_reorg_error(&err)); + assert_eq!(reorg_error_class(&err), ReorgErrorClass::InvalidBlock); + } + + #[test] + fn semantic_state_error_rejects_tip_without_stopping_net_task() { let err = Error::State(Box::new(state::Error::NotEnoughFees)); assert!(!is_fatal_reorg_error(&err)); + assert_eq!(reorg_error_class(&err), ReorgErrorClass::Rejected); + } + + #[test] + fn mainchain_reconciliation_state_error_is_fatal() { + let err = classify_mainchain_reconciliation_error(Error::State( + Box::new(state::Error::NotEnoughFees), + )); + assert!(matches!(err, Error::MainchainReconciliationState(_))); + assert!(is_fatal_reorg_error(&err)); + assert_eq!(reorg_error_class(&err), ReorgErrorClass::Fatal); + } + + #[test] + fn missing_metadata_blocks_without_stopping_net_task() { + let err = Error::MissingWithdrawalBundleMetadata { + event_block_hash: bitcoin::BlockHash::from_byte_array([1; 32]), + m6id: crate::types::M6id(bitcoin::Txid::from_byte_array([2; 32])), + }; + assert!(!is_fatal_reorg_error(&err)); + assert_eq!(reorg_error_class(&err), ReorgErrorClass::Blocked); } - // local infrastructure errors stay fatal #[test] fn infrastructure_error_is_fatal() { assert!(is_fatal_reorg_error(&Error::PeerInfoRxClosed)); + assert_eq!( + reorg_error_class(&Error::PeerInfoRxClosed), + ReorgErrorClass::Fatal + ); } } diff --git a/lib/state/block.rs b/lib/state/block.rs index 2c378591..01707ab5 100644 --- a/lib/state/block.rs +++ b/lib/state/block.rs @@ -6,10 +6,11 @@ use sneed::{RoTxn, RwTxn}; use crate::{ state::{Error, PrevalidatedBlock, State, amm, dutch_auction, error}, types::{ - AmountOverflowError, Authorization, BitAssetId, Body, FilledOutput, - FilledOutputContent, GetAddress as _, GetBitcoinValue as _, Header, - InPoint, OutPoint, OutPointKey, OutputContent, SpentOutput, TxData, - Verify as _, + AccumulatorDiff, AddressOutPointKey, AmountOverflowError, + Authorization, BitAssetId, Body, FilledOutput, FilledOutputContent, + GetAddress as _, GetBitcoinValue as _, Header, InPoint, MerkleRoot, + OutPoint, OutPointKey, Output, OutputContent, SpentOutput, TxData, + Verify as _, utreexo_leaf_hash, }, }; @@ -18,6 +19,78 @@ fn calculate_total_inputs(body: &Body) -> usize { body.transactions.iter().map(|t| t.inputs.len()).sum() } +fn fill_coinbase_output(output: &Output) -> Result { + let filled_content = match output.content.clone() { + OutputContent::Bitcoin(value) => FilledOutputContent::Bitcoin(value), + OutputContent::Withdrawal(withdrawal) => { + FilledOutputContent::BitcoinWithdrawal(withdrawal) + } + OutputContent::AmmLpToken(_) + | OutputContent::BitAsset(_) + | OutputContent::BitAssetControl + | OutputContent::BitAssetReservation + | OutputContent::DutchAuctionReceipt => { + return Err(Error::BadCoinbaseOutputContent); + } + }; + Ok(FilledOutput { + address: output.address, + content: filled_content, + memo: output.memo.clone(), + }) +} + +fn filled_coinbase_outputs( + merkle_root: MerkleRoot, + coinbase: &[Output], +) -> Result, Error> { + coinbase + .iter() + .enumerate() + .map(|(vout, output)| { + let outpoint = OutPoint::Coinbase { + merkle_root, + vout: vout as u32, + }; + Ok((outpoint, fill_coinbase_output(output)?)) + }) + .collect() +} + +fn build_accumulator_diff( + merkle_root: MerkleRoot, + body: &Body, + filled_transactions: &[crate::types::FilledTransaction], +) -> Result { + let mut accumulator_diff = AccumulatorDiff::default(); + for filled_tx in filled_transactions { + for (outpoint, output) in filled_tx.spent_inputs() { + accumulator_diff.remove(utreexo_leaf_hash(outpoint, output)); + } + } + for (outpoint, output) in + filled_coinbase_outputs(merkle_root, &body.coinbase)? + { + accumulator_diff.insert(utreexo_leaf_hash(&outpoint, &output)); + } + for filled_tx in filled_transactions { + let txid = filled_tx.txid(); + let Some(filled_outputs) = filled_tx.filled_outputs() else { + let err = error::FillTxOutputContents(Box::new(filled_tx.clone())); + return Err(err.into()); + }; + for (vout, filled_output) in filled_outputs.iter().enumerate() { + let outpoint = OutPoint::Regular { + txid, + vout: vout as u32, + }; + accumulator_diff + .insert(utreexo_leaf_hash(&outpoint, filled_output)); + } + } + Ok(accumulator_diff) +} + /// Validate a block, returning fees pub fn validate( state: &State, @@ -181,6 +254,12 @@ pub fn prevalidate( let () = Authorization::verify_body(body).map_err(Error::Authorization)?; + let accumulator_diff = build_accumulator_diff( + computed_merkle_root, + body, + &filled_transactions, + )?; + let height = state.try_get_height(rotxn)?.map_or(0, |height| height + 1); Ok(PrevalidatedBlock { @@ -189,6 +268,7 @@ pub fn prevalidate( total_fees, coinbase_value, next_height: height, + accumulator_diff, }) } @@ -197,7 +277,7 @@ pub fn connect_prevalidated( rwtxn: &mut RwTxn, header: &Header, body: &Body, - prevalidated: PrevalidatedBlock, + prevalidated: &PrevalidatedBlock, ) -> Result<(), Error> { // Skip validation - already done in prevalidate // Use precomputed values to avoid redundant DB reads @@ -215,39 +295,15 @@ pub fn connect_prevalidated( .sum::() + body.coinbase.len(); - // Use Vec + sort_unstable instead of individual DB operations for better performance - let mut utxo_deletes: Vec = Vec::with_capacity(total_inputs); let mut stxo_puts: Vec<(OutPointKey, SpentOutput)> = Vec::with_capacity(total_inputs); let mut utxo_puts: Vec<(OutPointKey, FilledOutput)> = Vec::with_capacity(total_outputs); // Collect coinbase outputs - for (vout, output) in body.coinbase.iter().enumerate() { - let outpoint = OutPoint::Coinbase { - merkle_root: header.merkle_root, - vout: vout as u32, - }; - let filled_content = match output.content.clone() { - OutputContent::Bitcoin(value) => { - FilledOutputContent::Bitcoin(value) - } - OutputContent::Withdrawal(withdrawal) => { - FilledOutputContent::BitcoinWithdrawal(withdrawal) - } - OutputContent::AmmLpToken(_) - | OutputContent::BitAsset(_) - | OutputContent::BitAssetControl - | OutputContent::BitAssetReservation - | OutputContent::DutchAuctionReceipt => { - return Err(Error::BadCoinbaseOutputContent); - } - }; - let filled_output = FilledOutput { - address: output.address, - content: filled_content, - memo: output.memo.clone(), - }; + for (outpoint, filled_output) in + filled_coinbase_outputs(header.merkle_root, &body.coinbase)? + { utxo_puts.push((OutPointKey::from_outpoint(&outpoint), filled_output)); } @@ -271,7 +327,6 @@ pub fn connect_prevalidated( vin: vin as u32, }, }; - utxo_deletes.push(key); stxo_puts.push((key, spent_output)); } @@ -366,21 +421,20 @@ pub fn connect_prevalidated( } // Sort all vectors in parallel for optimal cursor access - utxo_deletes.par_sort_unstable(); stxo_puts.par_sort_unstable_by_key(|(key, _)| *key); utxo_puts.par_sort_unstable_by_key(|(key, _)| *key); - // Apply all database operations using pre-sorted keys for optimal B-tree access - for key in &utxo_deletes { - state.utxos.delete(rwtxn, key)?; - } - - for (key, spent_output) in &stxo_puts { - state.stxos.put(rwtxn, key, spent_output)?; + for (key, output) in stxo_puts { + if !state.spend_utxo(rwtxn, key, output)? { + return Err(error::NoUtxo { + outpoint: key.to_outpoint(), + } + .into()); + } } - for (key, filled_output) in &utxo_puts { - state.utxos.put(rwtxn, key, filled_output)?; + for (key, filled_output) in utxo_puts { + state.put_utxo(rwtxn, key, filled_output, prevalidated.next_height)?; } // Update tip and height using precomputed values @@ -441,7 +495,7 @@ pub fn connect( content: filled_content, memo: output.memo.clone(), }; - state.utxos.put(rwtxn, &outpoint_key, &filled_output)?; + state.put_utxo(rwtxn, outpoint_key, filled_output, height)?; } for transaction in &body.transactions { let filled_tx = state.fill_transaction(rwtxn, transaction)?; @@ -459,20 +513,21 @@ pub fn connect( vin: vin as u32, }, }; - state.utxos.delete(rwtxn, &input_key)?; - state.stxos.put(rwtxn, &input_key, &spent_output)?; + if !state.spend_utxo(rwtxn, input_key, spent_output)? { + return Err(error::NoUtxo { outpoint: *input }.into()); + } } let Some(filled_outputs) = filled_tx.filled_outputs() else { let err = error::FillTxOutputContents(Box::new(filled_tx)); return Err(err.into()); }; - for (vout, filled_output) in filled_outputs.iter().enumerate() { + for (vout, filled_output) in filled_outputs.into_iter().enumerate() { let outpoint = OutPoint::Regular { txid, vout: vout as u32, }; let outpoint_key = OutPointKey::from_outpoint(&outpoint); - state.utxos.put(rwtxn, &outpoint_key, filled_output)?; + state.put_utxo(rwtxn, outpoint_key, filled_output, height)?; } match &transaction.data { None => (), @@ -654,13 +709,15 @@ pub fn disconnect_tip( } // delete UTXOs, last-to-first tx.outputs.iter().enumerate().rev().try_for_each( - |(vout, _output)| { + |(vout, output)| { let outpoint = OutPoint::Regular { txid, vout: vout as u32, }; let outpoint_key = OutPointKey::from_outpoint(&outpoint); - if state.utxos.delete(rwtxn, &outpoint_key)? { + let address_key = + AddressOutPointKey::new(output.address, outpoint_key); + if state.delete_utxo(rwtxn, &address_key)? { Ok::<_, Error>(()) } else { Err(error::NoUtxo { outpoint }.into()) @@ -670,13 +727,7 @@ pub fn disconnect_tip( // unspend STXOs, last-to-first tx.inputs.iter().rev().try_for_each(|outpoint| { let outpoint_key = OutPointKey::from_outpoint(outpoint); - if let Some(spent_output) = - state.stxos.try_get(rwtxn, &outpoint_key)? - { - state.stxos.delete(rwtxn, &outpoint_key)?; - state - .utxos - .put(rwtxn, &outpoint_key, &spent_output.output)?; + if state.unspend_utxo(rwtxn, &outpoint_key)? { Ok(()) } else { Err(Error::NoStxo { @@ -686,20 +737,24 @@ pub fn disconnect_tip( }) })?; // delete coinbase UTXOs, last-to-first - body.coinbase.iter().enumerate().rev().try_for_each( - |(vout, _output)| { + body.coinbase + .iter() + .enumerate() + .rev() + .try_for_each(|(vout, output)| { let outpoint = OutPoint::Coinbase { merkle_root: header.merkle_root, vout: vout as u32, }; let outpoint_key = OutPointKey::from_outpoint(&outpoint); - if state.utxos.delete(rwtxn, &outpoint_key)? { + let address_key = + AddressOutPointKey::new(output.address, outpoint_key); + if state.delete_utxo(rwtxn, &address_key)? { Ok::<_, Error>(()) } else { Err(error::NoUtxo { outpoint }.into()) } - }, - )?; + })?; match (header.prev_side_hash, height) { (None, 0) => { state.tip.delete(rwtxn, &())?; @@ -722,13 +777,17 @@ mod test { authorization::{self, SigningKey}, state::{ BitAssetSeqId, - block::{connect, disconnect_tip, validate}, + block::{ + build_accumulator_diff, connect, disconnect_tip, validate, + }, test::fresh_state, }, types::{ - BitAssetData, BitAssetDataUpdates, BitAssetId, BlockHash, Body, - FilledOutput, FilledOutputContent, Hash, Header, OutPoint, + AccumulatorDiff, Address, BitAssetData, BitAssetDataUpdates, + BitAssetId, BitcoinOutputContent, BlockHash, Body, FilledOutput, + FilledOutputContent, FilledTransaction, Hash, Header, OutPoint, OutPointKey, Output, OutputContent, Transaction, TxData, Update, + utreexo_leaf_hash, }, }; @@ -753,6 +812,76 @@ mod test { } } + #[test] + fn accumulator_diff_tracks_body_utxo_changes() -> anyhow::Result<()> { + let spent_outpoint = OutPoint::Regular { + txid: [1; 32].into(), + vout: 0, + }; + let spent_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(10), + ); + let transaction = Transaction { + inputs: vec![spent_outpoint], + outputs: vec![Output::new( + Address::ALL_ZEROS, + OutputContent::Bitcoin(BitcoinOutputContent( + bitcoin::Amount::from_sat(10), + )), + )], + ..Default::default() + }; + let filled_transaction = FilledTransaction { + transaction: transaction.clone(), + spent_utxos: vec![spent_output.clone()], + }; + let body = Body { + coinbase: vec![Output::new( + Address::ALL_ZEROS, + OutputContent::Bitcoin(BitcoinOutputContent( + bitcoin::Amount::ZERO, + )), + )], + transactions: vec![transaction], + authorizations: Vec::new(), + }; + let merkle_root = + Body::compute_merkle_root(&body.coinbase, &body.transactions); + + let diff = build_accumulator_diff( + merkle_root, + &body, + std::slice::from_ref(&filled_transaction), + )?; + + let mut expected = AccumulatorDiff::default(); + expected.remove(utreexo_leaf_hash(&spent_outpoint, &spent_output)); + let coinbase_outpoint = OutPoint::Coinbase { + merkle_root, + vout: 0, + }; + let coinbase_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::ZERO, + ); + expected + .insert(utreexo_leaf_hash(&coinbase_outpoint, &coinbase_output)); + let regular_outpoint = OutPoint::Regular { + txid: filled_transaction.txid(), + vout: 0, + }; + let regular_output = FilledOutput::new_bitcoin_value( + Address::ALL_ZEROS, + bitcoin::Amount::from_sat(10), + ); + expected.insert(utreexo_leaf_hash(®ular_outpoint, ®ular_output)); + + assert_eq!(diff, expected); + assert_eq!(diff.counts(), (2, 1)); + Ok(()) + } + #[test] fn disconnect_bitasset_data_update() -> anyhow::Result<()> { let (_temp_dir, env, state) = @@ -818,23 +947,25 @@ mod test { )?; let seq = state.bitassets.next_seq(&rwtxn)?; anyhow::ensure!(seq == BitAssetSeqId(1)); - state.utxos.put( + state.put_utxo( &mut rwtxn, - &OutPointKey::from_outpoint(&bitasset_outpoint), - &FilledOutput { + OutPointKey::from_outpoint(&bitasset_outpoint), + FilledOutput { address, content: FilledOutputContent::BitAsset(bitasset_id, 5), memo: Vec::new(), }, + 0, )?; - state.utxos.put( + state.put_utxo( &mut rwtxn, - &OutPointKey::from_outpoint(&control_outpoint), - &FilledOutput { + OutPointKey::from_outpoint(&control_outpoint), + FilledOutput { address, content: FilledOutputContent::BitAssetControl(bitasset_id), memo: Vec::new(), }, + 0, )?; rwtxn.commit()?; } diff --git a/lib/state/error.rs b/lib/state/error.rs index 02f4b64c..33eab622 100644 --- a/lib/state/error.rs +++ b/lib/state/error.rs @@ -235,6 +235,13 @@ pub enum ConnectWithdrawalBundleSubmitted { "dropped withdrawal bundle {0} marked as pending in withdrawal_bundles" )] DroppedPending(M6id), + #[error( + "withdrawal bundle metadata missing for submission {m6id} in {event_block_hash}" + )] + MissingWithdrawalBundleMetadata { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + }, #[error(transparent)] NoUtxo(#[from] NoUtxo), #[error(transparent)] @@ -364,6 +371,8 @@ pub enum Error { ConnectWithdrawalBundleSubmitted(#[from] ConnectWithdrawalBundleSubmitted), #[error(transparent)] Db(Box), + #[error("{outpoint} already exists")] + DuplicateDeposit { outpoint: OutPoint }, #[error(transparent)] DutchAuction(#[from] DutchAuction), #[error(transparent)] @@ -443,6 +452,13 @@ pub enum Error { m6id: M6id, outpoint: OutPoint, }, + #[error( + "withdrawal bundle metadata missing for event {m6id} in {event_block_hash}" + )] + MissingWithdrawalBundleMetadata { + event_block_hash: bitcoin::BlockHash, + m6id: M6id, + }, #[error("Unknown withdrawal bundle: {m6id}")] UnknownWithdrawalBundle { m6id: M6id }, #[error( @@ -472,3 +488,25 @@ impl From for Error { Self::Db(Box::new(err)) } } + +impl Error { + /// Return the L1 event that cannot be applied until full withdrawal bundle + /// metadata is recovered. + pub fn missing_withdrawal_bundle_metadata( + &self, + ) -> Option<(bitcoin::BlockHash, M6id)> { + match self { + Self::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + } + | Self::ConnectWithdrawalBundleSubmitted( + ConnectWithdrawalBundleSubmitted::MissingWithdrawalBundleMetadata { + event_block_hash, + m6id, + }, + ) => Some((*event_block_hash, *m6id)), + _ => None, + } + } +} diff --git a/lib/state/mod.rs b/lib/state/mod.rs index 679dc55f..660202b9 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -1,21 +1,26 @@ -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::Arc, +}; use fallible_iterator::FallibleIterator as _; use futures::Stream; use heed::types::SerdeBincode; use itertools::Itertools; +use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use sneed::{DatabaseUnique, RoDatabaseUnique, RoTxn, RwTxn, UnitKey}; use crate::{ authorization::Authorization, types::{ - Address, AmountOverflowError, Authorized, AuthorizedTransaction, - BitAssetId, BlockHash, Body, FilledOutput, FilledTransaction, - GetAddress as _, GetBitcoinValue as _, Header, InPoint, M6id, OutPoint, - OutPointKey, SpentOutput, Transaction, TxData, VERSION, Verify as _, - Version, WithdrawalBundle, WithdrawalBundleStatus, - proto::mainchain::TwoWayPegData, + Accumulator, Address, AddressOutPointKey, AmountOverflowError, + Authorized, AuthorizedTransaction, BitAssetId, BlockHash, Body, + FilledOutput, FilledTransaction, GetAddress as _, GetBitcoinValue as _, + Header, InPoint, M6id, OutPoint, OutPointKey, SpentOutput, Transaction, + TxData, VERSION, Verify as _, Version, WithdrawalBundle, + WithdrawalBundleMetadata, WithdrawalBundleStatus, + proto::mainchain::{BlockInfo, TwoWayPegData}, }, util::Watchable, }; @@ -44,6 +49,7 @@ pub struct PrevalidatedBlock { pub total_fees: bitcoin::Amount, pub coinbase_value: bitcoin::Amount, pub next_height: u32, // Precomputed next height to avoid DB read in write txn + pub accumulator_diff: crate::types::AccumulatorDiff, } /// Information we have regarding a withdrawal bundle @@ -68,6 +74,12 @@ type WithdrawalBundlesDb = DatabaseUnique< )>, >; +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct SpentUtxoEntry { + pub created_height: u32, + pub output: SpentOutput, +} + #[derive(Clone)] pub struct State { /// Current tip @@ -80,7 +92,9 @@ pub struct State { /// Associates Dutch auction sequence numbers with auction state dutch_auctions: dutch_auction::Db, utxos: DatabaseUnique>, - stxos: DatabaseUnique>, + utxo_heights_by_address: + DatabaseUnique>, + stxos: DatabaseUnique>, /// Pending withdrawal bundle. MUST exist in withdrawal_bundles pending_withdrawal_bundle: DatabaseUnique>, /// Latest failed (known) withdrawal bundle @@ -90,6 +104,16 @@ pub struct State { /// Some withdrawal bundles may be unknown. /// in which case they are `None`. withdrawal_bundles: WithdrawalBundlesDb, + /// Branch-independent bundle metadata, keyed by the L1-visible m6id. + /// + /// This is an archival cache rather than rollbackable sidechain state. A + /// stale entry is harmless because it is only used after a matching L1 + /// submission event, while retaining entries allows an L2-only reorg to + /// reapply the exact withdrawal reservation. + withdrawal_bundle_archive: DatabaseUnique< + SerdeBincode, + SerdeBincode, + >, /// Deposit blocks and the height at which they were applied, keyed sequentially deposit_blocks: DatabaseUnique< SerdeBincode, @@ -100,13 +124,17 @@ pub struct State { SerdeBincode, SerdeBincode<(bitcoin::BlockHash, u32)>, >, + pub utreexo_accumulator: Arc>, _version: DatabaseUnique>, } impl State { - pub const NUM_DBS: u32 = bitassets::Dbs::NUM_DBS + 12; + pub const NUM_DBS: u32 = bitassets::Dbs::NUM_DBS + 14; - pub fn new(env: &sneed::Env) -> Result { + pub fn new( + env: &sneed::Env, + utreexo_accumulator: Accumulator, + ) -> Result { let mut rwtxn = env.write_txn()?; let tip = DatabaseUnique::create(env, &mut rwtxn, "tip")?; let height = DatabaseUnique::create(env, &mut rwtxn, "height")?; @@ -115,6 +143,8 @@ impl State { let dutch_auctions = DatabaseUnique::create(env, &mut rwtxn, "dutch_auctions")?; let utxos = DatabaseUnique::create(env, &mut rwtxn, "utxos")?; + let utxo_heights_by_address = + DatabaseUnique::create(env, &mut rwtxn, "utxo_heights_by_address")?; let stxos = DatabaseUnique::create(env, &mut rwtxn, "stxos")?; let pending_withdrawal_bundle = DatabaseUnique::create( env, @@ -128,6 +158,11 @@ impl State { )?; let withdrawal_bundles = DatabaseUnique::create(env, &mut rwtxn, "withdrawal_bundles")?; + let withdrawal_bundle_archive = DatabaseUnique::create( + env, + &mut rwtxn, + "withdrawal_bundle_archive", + )?; let deposit_blocks = DatabaseUnique::create(env, &mut rwtxn, "deposit_blocks")?; let withdrawal_bundle_event_blocks = DatabaseUnique::create( @@ -147,12 +182,15 @@ impl State { bitassets, dutch_auctions, utxos, + utxo_heights_by_address, stxos, pending_withdrawal_bundle, latest_failed_withdrawal_bundle, withdrawal_bundles, + withdrawal_bundle_archive, withdrawal_bundle_event_blocks, deposit_blocks, + utreexo_accumulator: Arc::new(Mutex::new(utreexo_accumulator)), _version: version, }) } @@ -180,10 +218,78 @@ impl State { pub fn stxos( &self, - ) -> &RoDatabaseUnique> { + ) -> &RoDatabaseUnique> { &self.stxos } + fn put_utxo( + &self, + rwtxn: &mut RwTxn, + outpoint_key: OutPointKey, + output: FilledOutput, + created_height: u32, + ) -> Result { + let address_key = AddressOutPointKey::new(output.address, outpoint_key); + self.utxo_heights_by_address.put( + rwtxn, + &address_key, + &created_height, + )?; + self.utxos.put(rwtxn, &outpoint_key, &output)?; + Ok(address_key) + } + + fn delete_utxo( + &self, + rwtxn: &mut RwTxn, + address_key: &AddressOutPointKey, + ) -> Result { + self.utxo_heights_by_address.delete(rwtxn, address_key)?; + Ok(self.utxos.delete(rwtxn, &address_key.outpoint_key())?) + } + + fn spend_utxo( + &self, + rwtxn: &mut RwTxn, + outpoint_key: OutPointKey, + spent_output: SpentOutput, + ) -> Result { + let address_key = + AddressOutPointKey::new(spent_output.output.address, outpoint_key); + let Some(created_height) = + self.utxo_heights_by_address.try_get(rwtxn, &address_key)? + else { + return Ok(false); + }; + + let entry = SpentUtxoEntry { + created_height, + output: spent_output, + }; + + self.stxos.put(rwtxn, &outpoint_key, &entry)?; + self.delete_utxo(rwtxn, &address_key) + } + + fn unspend_utxo( + &self, + rwtxn: &mut RwTxn, + outpoint_key: &OutPointKey, + ) -> Result { + let Some(entry) = self.stxos.try_get(rwtxn, outpoint_key)? else { + return Ok(false); + }; + + self.put_utxo( + rwtxn, + *outpoint_key, + entry.output.output, + entry.created_height, + )?; + + Ok(self.stxos.delete(rwtxn, outpoint_key)?) + } + pub fn withdrawal_bundle_event_blocks( &self, ) -> &RoDatabaseUnique< @@ -222,13 +328,25 @@ impl State { &self, rotxn: &RoTxn, addresses: &HashSet
, + height_threshold: u32, ) -> Result, Error> { - let utxos: HashMap = self - .utxos - .iter(rotxn)? - .filter(|(_, output)| Ok(addresses.contains(&output.address))) - .map(|(key, output)| Ok((key.to_outpoint(), output))) - .collect()?; + let mut utxos = HashMap::new(); + for address in addresses { + let start = AddressOutPointKey::start(*address); + let end = AddressOutPointKey::end(*address); + let mut iter = self + .utxo_heights_by_address + .range(rotxn, &(start..=end)) + .map_err(sneed::db::Error::from)?; + while let Some((key, created_height)) = iter.next()? { + if created_height >= height_threshold { + let outpoint_key = key.outpoint_key(); + let outpoint = outpoint_key.to_outpoint(); + let output = self.utxos.get(rotxn, &outpoint_key)?; + utxos.insert(outpoint, output); + } + } + } Ok(utxos) } @@ -300,13 +418,13 @@ impl State { .try_get(rotxn, &key)? .ok_or(Error::NoStxo { outpoint: *input })?; assert_eq!( - stxo.inpoint, + stxo.output.inpoint, InPoint::Regular { txid, vin: vin as u32 } ); - spent_utxos.push(stxo.output); + spent_utxos.push(stxo.output.output); } spent_utxos.reverse(); Ok(FilledTransaction { @@ -351,6 +469,24 @@ impl State { Ok(Some((bundle, height))) } + pub(crate) fn try_get_withdrawal_bundle_metadata( + &self, + rotxn: &RoTxn, + m6id: M6id, + ) -> Result, Error> { + Ok(self.withdrawal_bundle_archive.try_get(rotxn, &m6id)?) + } + + pub(crate) fn put_withdrawal_bundle_metadata( + &self, + rwtxn: &mut RwTxn, + metadata: &WithdrawalBundleMetadata, + ) -> Result<(), Error> { + let m6id = metadata.compute_m6id(); + self.withdrawal_bundle_archive.put(rwtxn, &m6id, metadata)?; + Ok(()) + } + /// Check that /// * If the tx is a BitAsset reservation, then the number of bitasset /// reservations in the outputs is exactly one more than the number of @@ -689,7 +825,8 @@ impl State { let mut total_deposit_stxo_value = bitcoin::Amount::ZERO; let mut total_withdrawal_stxo_value = bitcoin::Amount::ZERO; self.stxos.iter(rotxn)?.map_err(Error::from).for_each( - |(outpoint_key, spent_output)| { + |(outpoint_key, spent_entry)| { + let spent_output = spent_entry.output; let outpoint = outpoint_key.to_outpoint(); if let OutPoint::Deposit(_) = outpoint { total_deposit_stxo_value = total_deposit_stxo_value @@ -743,10 +880,37 @@ impl State { &self, rwtxn: &mut RwTxn, two_way_peg_data: &TwoWayPegData, - ) -> Result<(), Error> { + ) -> Result { two_way_peg_data::connect(self, rwtxn, two_way_peg_data) } + pub(crate) fn begin_connect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + ) -> Result { + two_way_peg_data::begin_connect(self, rwtxn) + } + + pub(crate) fn connect_two_way_peg_block_info( + &self, + rwtxn: &mut RwTxn, + block_hash: bitcoin::BlockHash, + block_info: &BlockInfo, + context: &mut two_way_peg_data::ConnectContext, + ) -> Result<(), Error> { + two_way_peg_data::connect_block_info( + self, rwtxn, block_hash, block_info, context, + ) + } + + pub(crate) fn finish_connect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + context: two_way_peg_data::ConnectContext, + ) -> Result { + two_way_peg_data::finish_connect(self, rwtxn, context) + } + pub fn disconnect_two_way_peg_data( &self, rwtxn: &mut RwTxn, @@ -755,6 +919,33 @@ impl State { two_way_peg_data::disconnect(self, rwtxn, two_way_peg_data) } + pub(crate) fn begin_disconnect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + ) -> Result { + two_way_peg_data::begin_disconnect(self, rwtxn) + } + + pub(crate) fn disconnect_two_way_peg_block_info( + &self, + rwtxn: &mut RwTxn, + block_hash: bitcoin::BlockHash, + block_info: &BlockInfo, + context: &mut two_way_peg_data::DisconnectContext, + ) -> Result<(), Error> { + two_way_peg_data::disconnect_block_info( + self, rwtxn, block_hash, block_info, context, + ) + } + + pub(crate) fn finish_disconnect_two_way_peg_data( + &self, + rwtxn: &mut RwTxn, + context: two_way_peg_data::DisconnectContext, + ) -> Result<(), Error> { + two_way_peg_data::finish_disconnect(self, rwtxn, context) + } + pub fn prevalidate_block( &self, rotxn: &RoTxn, @@ -769,7 +960,7 @@ impl State { rwtxn: &mut RwTxn, header: &Header, body: &Body, - prevalidated: PrevalidatedBlock, + prevalidated: &PrevalidatedBlock, ) -> Result<(), Error> { block::connect_prevalidated(self, rwtxn, header, body, prevalidated) } @@ -781,7 +972,7 @@ impl State { body: &Body, ) -> Result<(), Error> { let prevalidated = self.prevalidate_block(rwtxn, header, body)?; - self.connect_prevalidated_block(rwtxn, header, body, prevalidated)?; + self.connect_prevalidated_block(rwtxn, header, body, &prevalidated)?; Ok(()) } } @@ -804,10 +995,11 @@ mod test { authorization, state::{Error, State, error}, types::{ - Address, AuthorizedTransaction, BitAssetData, BitAssetId, - FilledOutput, FilledOutputContent, FilledTransaction, Hash, - InPoint, OutPoint, OutPointKey, Output, OutputContent, SpentOutput, - Transaction, TxData, Txid, VerifyingKey, WithdrawalOutputContent, + Accumulator, Address, AuthorizedTransaction, BitAssetData, + BitAssetId, FilledOutput, FilledOutputContent, FilledTransaction, + Hash, InPoint, OutPoint, OutPointKey, Output, OutputContent, + SpentOutput, Transaction, TxData, Txid, VerifyingKey, + WithdrawalOutputContent, }, }; @@ -837,7 +1029,7 @@ mod test { test_name: &str, ) -> anyhow::Result<(temp_dir::TempDir, sneed::Env, State)> { let (temp_dir, env) = temp_env(test_name)?; - let state = State::new(&env)?; + let state = State::new(&env, Accumulator::default())?; Ok((temp_dir, env, state)) } @@ -1082,11 +1274,14 @@ mod test { txid: bitcoin::Txid::from_byte_array([i; 32]), vout: 0, }); - let stxo = SpentOutput { - output: bitcoin_filled_output(Address::ALL_ZEROS, sats), - inpoint: InPoint::Regular { - txid: [i; 32].into(), - vin: 0, + let stxo = super::SpentUtxoEntry { + created_height: 0, + output: SpentOutput { + output: bitcoin_filled_output(Address::ALL_ZEROS, sats), + inpoint: InPoint::Regular { + txid: [i; 32].into(), + vin: 0, + }, }, }; state @@ -1100,12 +1295,15 @@ mod test { txid: [i; 32].into(), vout: 0, }; - let stxo = SpentOutput { - output: bitcoin_filled_output(Address::ALL_ZEROS, sats), - inpoint: InPoint::Withdrawal { - m6id: crate::types::M6id( - bitcoin::Txid::from_byte_array([i; 32]), - ), + let stxo = super::SpentUtxoEntry { + created_height: 0, + output: SpentOutput { + output: bitcoin_filled_output(Address::ALL_ZEROS, sats), + inpoint: InPoint::Withdrawal { + m6id: crate::types::M6id( + bitcoin::Txid::from_byte_array([i; 32]), + ), + }, }, }; state @@ -1130,4 +1328,77 @@ mod test { ); Ok(()) } + + #[test] + fn address_utxo_index_stores_height_and_restores_on_unspend() + -> anyhow::Result<()> { + use std::collections::HashSet; + + let (_, env, state) = fresh_state( + "address_utxo_index_stores_height_and_restores_on_unspend", + )?; + let address = Address([1; 20]); + let outpoint = OutPoint::Regular { + txid: Txid([2; 32]), + vout: 0, + }; + let key = OutPointKey::from_outpoint(&outpoint); + let output = bitcoin_filled_output(address, 42); + let created_height = 7; + let addresses = HashSet::from([address]); + + { + let mut rwtxn = env.write_txn()?; + let address_key = state.put_utxo( + &mut rwtxn, + key, + output.clone(), + created_height, + )?; + anyhow::ensure!( + state.utxo_heights_by_address.get(&rwtxn, &address_key)? + == created_height + ); + rwtxn.commit()?; + } + + { + let rotxn = env.read_txn()?; + let indexed = + state.get_utxos_by_addresses(&rotxn, &addresses, 0)?; + anyhow::ensure!(indexed.get(&outpoint) == Some(&output)); + let above_height = state.get_utxos_by_addresses( + &rotxn, + &addresses, + created_height + 1, + )?; + anyhow::ensure!(above_height.is_empty()); + } + + { + let mut rwtxn = env.write_txn()?; + let spent_output = SpentOutput { + output: output.clone(), + inpoint: InPoint::Regular { + txid: Txid([3; 32]), + vin: 0, + }, + }; + anyhow::ensure!(state.spend_utxo(&mut rwtxn, key, spent_output)?); + anyhow::ensure!(state.unspend_utxo(&mut rwtxn, &key)?); + rwtxn.commit()?; + } + + let rotxn = env.read_txn()?; + let restored = + state.get_utxos_by_addresses(&rotxn, &addresses, created_height)?; + anyhow::ensure!(restored.get(&outpoint) == Some(&output)); + let above_height = state.get_utxos_by_addresses( + &rotxn, + &addresses, + created_height + 1, + )?; + anyhow::ensure!(above_height.is_empty()); + Ok(()) + } } diff --git a/lib/state/two_way_peg_data.rs b/lib/state/two_way_peg_data.rs index 72cdc5ab..3ba2d5bf 100644 --- a/lib/state/two_way_peg_data.rs +++ b/lib/state/two_way_peg_data.rs @@ -12,70 +12,119 @@ use crate::{ rollback::{HeightStamped, RollBack}, }, types::{ - AggregatedWithdrawal, AmountOverflowError, FilledOutput, - FilledOutputContent, InPoint, M6id, OutPoint, OutPointKey, SpentOutput, - WithdrawalBundle, WithdrawalBundleEvent, WithdrawalBundleEventStatus, + AccumulatorDiff, AddressOutPointKey, AggregatedWithdrawal, + AmountOverflowError, FilledOutput, FilledOutputContent, InPoint, M6id, + OutPoint, OutPointKey, SpentOutput, WithdrawalBundle, + WithdrawalBundleEvent, WithdrawalBundleEventStatus, WithdrawalBundleStatus, WithdrawalOutputContent, - proto::mainchain::{BlockEvent, TwoWayPegData}, + proto::mainchain::{BlockEvent, BlockInfo, TwoWayPegData}, + utreexo_leaf_hash, }, }; +fn diff_add( + diff: &mut AccumulatorDiff, + outpoint: &OutPoint, + output: &FilledOutput, +) { + diff.insert(utreexo_leaf_hash(outpoint, output)); +} + +fn diff_remove( + diff: &mut AccumulatorDiff, + outpoint: &OutPoint, + output: &FilledOutput, +) { + diff.remove(utreexo_leaf_hash(outpoint, output)); +} + +pub(crate) struct ConnectContext { + block_height: u32, + latest_deposit_block_hash: Option, + latest_withdrawal_bundle_event_block_hash: Option, + accumulator_diff: AccumulatorDiff, +} + fn collect_withdrawal_bundle( state: &State, txn: &RoTxn, block_height: u32, ) -> Result, Error> { - // Aggregate all outputs by destination. + collect_withdrawal_bundle_with_max_inputs( + state, + txn, + block_height, + WithdrawalBundle::MAX_INPUTS, + ) +} + +fn collect_withdrawal_bundle_with_max_inputs( + state: &State, + txn: &RoTxn, + block_height: u32, + max_inputs: usize, +) -> Result, Error> { + let max_inputs = max_inputs.min(WithdrawalBundle::MAX_INPUTS); + // Select a deterministic, bounded prefix before aggregating by + // destination. Once a bundle is submitted those UTXOs leave the set, so + // later bundles naturally continue with the remaining withdrawals. // destination -> (value, mainchain fee, spent_utxos) let mut address_to_aggregated_withdrawal = HashMap::< bitcoin::Address, AggregatedWithdrawal, >::new(); - state.utxos.iter(txn)?.map_err(Error::from).for_each( - |(outpoint, output)| { - if let FilledOutputContent::BitcoinWithdrawal( - WithdrawalOutputContent { - value, - ref main_address, - main_fee, - }, - ) = output.content - { - let aggregated = address_to_aggregated_withdrawal - .entry(main_address.clone()) - .or_insert(AggregatedWithdrawal { - spend_utxos: HashMap::new(), - main_address: main_address.clone(), - value: bitcoin::Amount::ZERO, - main_fee: bitcoin::Amount::ZERO, - }); - // Add up all values. - aggregated.value = aggregated - .value - .checked_add(value) - .ok_or(AmountOverflowError)?; - aggregated.main_fee = aggregated - .main_fee - .checked_add(main_fee) - .ok_or(AmountOverflowError)?; - aggregated - .spend_utxos - .insert(outpoint.to_outpoint(), output); - } - Ok(()) - }, - )?; + let mut utxos = state.utxos.iter(txn)?.map_err(Error::from); + let mut selected_inputs = 0; + while selected_inputs < max_inputs { + let Some((outpoint, output)) = utxos.next()? else { + break; + }; + if let FilledOutputContent::BitcoinWithdrawal( + WithdrawalOutputContent { + value, + ref main_address, + main_fee, + }, + ) = output.content + { + let aggregated = address_to_aggregated_withdrawal + .entry(main_address.clone()) + .or_insert(AggregatedWithdrawal { + spend_utxos: BTreeMap::new(), + main_address: main_address.clone(), + value: bitcoin::Amount::ZERO, + main_fee: bitcoin::Amount::ZERO, + }); + // Add up all values. + aggregated.value = aggregated + .value + .checked_add(value) + .ok_or(AmountOverflowError)?; + aggregated.main_fee = aggregated + .main_fee + .checked_add(main_fee) + .ok_or(AmountOverflowError)?; + aggregated + .spend_utxos + .insert(outpoint.to_outpoint(), output); + selected_inputs += 1; + } + } if address_to_aggregated_withdrawal.is_empty() { return Ok(None); } let mut aggregated_withdrawals: Vec<_> = address_to_aggregated_withdrawal.into_values().collect(); - aggregated_withdrawals.sort_by_key(|a| std::cmp::Reverse(a.clone())); + aggregated_withdrawals.sort_by(|lhs, rhs| rhs.cmp(lhs)); let mut fee = bitcoin::Amount::ZERO; let mut spend_utxos = BTreeMap::::new(); let mut bundle_outputs = Vec::new(); let mut bundle_txouts_size: u32 = 0; - for aggregated in &aggregated_withdrawals { + for aggregated in aggregated_withdrawals { + let remaining_inputs = max_inputs.saturating_sub(spend_utxos.len()); + if remaining_inputs == 0 { + break; + } let script_pubkey = aggregated.main_address.assume_checked_ref().script_pubkey(); let Ok(n_outputs) = u32::try_from(bundle_outputs.len() + 1) else { @@ -101,13 +150,35 @@ fn collect_withdrawal_bundle( { break; } + let selected_spend_utxos: BTreeMap<_, _> = aggregated + .spend_utxos + .into_iter() + .take(remaining_inputs) + .collect(); + let mut selected_value = bitcoin::Amount::ZERO; + let mut selected_fee = bitcoin::Amount::ZERO; + for output in selected_spend_utxos.values() { + let FilledOutputContent::BitcoinWithdrawal(withdrawal) = + &output.content + else { + unreachable!( + "aggregated withdrawal contains non-withdrawal output" + ) + }; + selected_value = selected_value + .checked_add(withdrawal.value) + .ok_or(AmountOverflowError)?; + selected_fee = selected_fee + .checked_add(withdrawal.main_fee) + .ok_or(AmountOverflowError)?; + } let bundle_output = bitcoin::TxOut { - value: aggregated.value, + value: selected_value, script_pubkey, }; - spend_utxos.extend(aggregated.spend_utxos.clone()); + spend_utxos.extend(selected_spend_utxos); bundle_outputs.push(bundle_output); - fee += aggregated.main_fee; + fee += selected_fee; } let bundle = WithdrawalBundle::new(block_height, fee, spend_utxos, bundle_outputs)?; @@ -128,6 +199,7 @@ fn connect_withdrawal_bundle_submitted( block_height: u32, event_block_hash: &bitcoin::BlockHash, m6id: M6id, + accumulator_diff: &mut AccumulatorDiff, ) -> Result<(), error::ConnectWithdrawalBundleSubmitted> { if let Some(bundle_m6id) = state.pending_withdrawal_bundle.try_get(rwtxn, &())? @@ -150,19 +222,19 @@ fn connect_withdrawal_bundle_submitted( return Err(err.into()); } }; - for (outpoint, spend_output) in bundle.spend_utxos() { - let outpoint_key = OutPointKey::from_outpoint(outpoint); - if !state.utxos.delete(rwtxn, &outpoint_key)? { + state.withdrawal_bundle_archive.put( + rwtxn, + &m6id, + &bundle.metadata(), + )?; + for (outpoint, output) in bundle.spend_utxos() { + if !spend_withdrawal_utxo(state, rwtxn, m6id, outpoint, output)? { return Err(error::NoUtxo { outpoint: *outpoint, } .into()); - }; - let spent_output = SpentOutput { - output: spend_output.clone(), - inpoint: InPoint::Withdrawal { m6id }, - }; - state.stxos.put(rwtxn, &outpoint_key, &spent_output)?; + } + diff_remove(accumulator_diff, outpoint, output); } assert_eq!( bundle_status.latest().value, @@ -264,23 +336,64 @@ fn connect_withdrawal_bundle_submitted( state .withdrawal_bundles .put(rwtxn, &m6id, &(bundle, bundle_status))? - } else { - tracing::warn!( + } else if let Some(archived_metadata) = + state.withdrawal_bundle_archive.try_get(rwtxn, &m6id)? + { + if !archived_metadata.has_valid_inputs_commitment() + || !archived_metadata.is_within_size_limit() + { + return Err( + error::ConnectWithdrawalBundleSubmitted::MissingWithdrawalBundleMetadata { + event_block_hash: *event_block_hash, + m6id, + }, + ); + } + let mut spend_utxos = BTreeMap::new(); + for outpoint in archived_metadata.spend_outpoints() { + let outpoint_key = OutPointKey::from_outpoint(outpoint); + let output = state.utxos.try_get(rwtxn, &outpoint_key)?.ok_or( + error::NoUtxo { + outpoint: *outpoint, + }, + )?; + spend_utxos.insert(*outpoint, output); + } + let bundle = archived_metadata + .with_spend_utxos(spend_utxos) + .expect("recovered spend UTXOs preserve archived outpoint keys"); + tracing::info!( %event_block_hash, %m6id, - "Unknown withdrawal bundle submitted" + "Recovered withdrawal bundle metadata for L1 submission" ); + for (outpoint, output) in bundle.spend_utxos() { + if !spend_withdrawal_utxo(state, rwtxn, m6id, outpoint, output)? { + return Err(error::NoUtxo { + outpoint: *outpoint, + } + .into()); + } + diff_remove(accumulator_diff, outpoint, output); + } state.withdrawal_bundles.put( rwtxn, &m6id, &( - WithdrawalBundleInfo::Unknown, + WithdrawalBundleInfo::Known(bundle), RollBack::>::new( WithdrawalBundleStatus::Submitted, block_height, ), ), )?; + } else { + return Err( + error::ConnectWithdrawalBundleSubmitted::MissingWithdrawalBundleMetadata { + event_block_hash: *event_block_hash, + m6id, + }, + ); }; Ok(()) } @@ -291,11 +404,16 @@ fn connect_withdrawal_bundle_confirmed( block_height: u32, event_block_hash: &bitcoin::BlockHash, m6id: M6id, + accumulator_diff: &mut AccumulatorDiff, ) -> Result<(), Error> { - let (mut bundle, mut bundle_status) = state - .withdrawal_bundles - .try_get(rwtxn, &m6id)? - .ok_or(Error::UnknownWithdrawalBundle { m6id })?; + let Some((bundle, mut bundle_status)) = + state.withdrawal_bundles.try_get(rwtxn, &m6id)? + else { + return Err(Error::MissingWithdrawalBundleMetadata { + event_block_hash: *event_block_hash, + m6id, + }); + }; if bundle_status.latest().value == WithdrawalBundleStatus::Confirmed { // Already applied return Ok(()); @@ -313,44 +431,10 @@ fn connect_withdrawal_bundle_confirmed( }); } WithdrawalBundleInfo::Unknown => { - // If an unknown bundle is confirmed, all UTXOs older than the - // bundle submission are potentially spent. - // This is only accepted in the case that block height is 0, - // and so no UTXOs could possibly have been double-spent yet. - // In this case, ALL UTXOs are considered spent. - if block_height == 0 { - tracing::warn!( - %event_block_hash, - %m6id, - "Unknown withdrawal bundle confirmed, marking all UTXOs as spent" - ); - let utxos: BTreeMap = state - .utxos - .iter(rwtxn) - .map_err(Error::from)? - .map(|(key, output)| Ok((key.into(), output))) - .collect()?; - for (outpoint, output) in &utxos { - let spent_output = SpentOutput { - output: output.clone(), - inpoint: InPoint::Withdrawal { m6id }, - }; - state.stxos.put( - rwtxn, - &OutPointKey::from(outpoint), - &spent_output, - )?; - } - state.utxos.clear(rwtxn)?; - bundle = WithdrawalBundleInfo::UnknownConfirmed { - spend_utxos: utxos, - }; - } else { - return Err(Error::UnknownWithdrawalBundleConfirmed { - event_block_hash: *event_block_hash, - m6id, - }); - } + return Err(Error::MissingWithdrawalBundleMetadata { + event_block_hash: *event_block_hash, + m6id, + }); } WithdrawalBundleInfo::Known(bundle) => { if matches!( @@ -366,8 +450,9 @@ fn connect_withdrawal_bundle_confirmed( "Unexpected withdrawal bundle confirmed, marking bundle UTXOs as spent" ); for (outpoint, output) in bundle.spend_utxos() { - let outpoint_key = OutPointKey::from(outpoint); - if !state.utxos.delete(rwtxn, &outpoint_key)? { + if !spend_withdrawal_utxo( + state, rwtxn, m6id, outpoint, output, + )? { return Err( Error::UnexpectedWithdrawalBundleInsolvency { event_block_hash: *event_block_hash, @@ -376,11 +461,7 @@ fn connect_withdrawal_bundle_confirmed( }, ); } - let spent_output = SpentOutput { - output: output.clone(), - inpoint: InPoint::Withdrawal { m6id }, - }; - state.stxos.put(rwtxn, &outpoint_key, &spent_output)?; + diff_remove(accumulator_diff, outpoint, output); } } } @@ -399,6 +480,7 @@ fn connect_withdrawal_bundle_failed( rwtxn: &mut RwTxn, block_height: u32, m6id: M6id, + accumulator_diff: &mut AccumulatorDiff, ) -> Result<(), Error> { tracing::debug!( %block_height, @@ -429,8 +511,12 @@ fn connect_withdrawal_bundle_failed( } for (outpoint, output) in bundle.spend_utxos() { let outpoint_key = OutPointKey::from_outpoint(outpoint); - state.stxos.delete(rwtxn, &outpoint_key)?; - state.utxos.put(rwtxn, &outpoint_key, output)?; + if !state.unspend_utxo(rwtxn, &outpoint_key)? { + return Err(Error::NoStxo { + outpoint: *outpoint, + }); + }; + diff_add(accumulator_diff, outpoint, output); } let latest_failed_m6id = if let Some(mut latest_failed_m6id) = state.latest_failed_withdrawal_bundle.try_get(rwtxn, &())? @@ -464,6 +550,7 @@ fn connect_withdrawal_bundle_event( block_height: u32, event_block_hash: &bitcoin::BlockHash, event: &WithdrawalBundleEvent, + accumulator_diff: &mut AccumulatorDiff, ) -> Result<(), Error> { match event.status { WithdrawalBundleEventStatus::Submitted => { @@ -473,6 +560,7 @@ fn connect_withdrawal_bundle_event( block_height, event_block_hash, event.m6id, + accumulator_diff, ) .map_err(Error::ConnectWithdrawalBundleSubmitted) } @@ -483,6 +571,7 @@ fn connect_withdrawal_bundle_event( block_height, event_block_hash, event.m6id, + accumulator_diff, ) } WithdrawalBundleEventStatus::Failed => { @@ -491,6 +580,7 @@ fn connect_withdrawal_bundle_event( rwtxn, block_height, event.m6id, + accumulator_diff, ) } } @@ -500,18 +590,23 @@ fn connect_2wpd_event( state: &State, rwtxn: &mut RwTxn, block_height: u32, - latest_deposit_block_hash: &mut Option, - latest_withdrawal_bundle_event_block_hash: &mut Option, event_block_hash: bitcoin::BlockHash, event: &BlockEvent, + context: &mut ConnectContext, ) -> Result<(), Error> { match event { BlockEvent::Deposit(deposit) => { let outpoint = OutPoint::Deposit(deposit.outpoint); let output = deposit.output.clone(); let outpoint_key = OutPointKey::from_outpoint(&outpoint); - state.utxos.put(rwtxn, &outpoint_key, &output)?; - *latest_deposit_block_hash = Some(event_block_hash); + if state.utxos.try_get(rwtxn, &outpoint_key)?.is_some() + || state.stxos.try_get(rwtxn, &outpoint_key)?.is_some() + { + return Err(Error::DuplicateDeposit { outpoint }); + } + state.put_utxo(rwtxn, outpoint_key, output, block_height)?; + diff_add(&mut context.accumulator_diff, &outpoint, &deposit.output); + context.latest_deposit_block_hash = Some(event_block_hash); } BlockEvent::WithdrawalBundle(withdrawal_bundle_event) => { let () = connect_withdrawal_bundle_event( @@ -520,38 +615,57 @@ fn connect_2wpd_event( block_height, &event_block_hash, withdrawal_bundle_event, + &mut context.accumulator_diff, )?; - *latest_withdrawal_bundle_event_block_hash = Some(event_block_hash); + context.latest_withdrawal_bundle_event_block_hash = + Some(event_block_hash); } } Ok(()) } -pub fn connect( +pub(crate) fn begin_connect( state: &State, rwtxn: &mut RwTxn, - two_way_peg_data: &TwoWayPegData, -) -> Result<(), Error> { +) -> Result { let block_height = state.try_get_height(rwtxn)?.ok_or(Error::NoTip)?; tracing::trace!(%block_height, "Connecting 2WPD..."); - // Handle deposits. - let mut latest_deposit_block_hash = None; - let mut latest_withdrawal_bundle_event_block_hash = None; - for (event_block_hash, event_block_info) in &two_way_peg_data.block_info { - for event in &event_block_info.events { - let () = connect_2wpd_event( - state, - rwtxn, - block_height, - &mut latest_deposit_block_hash, - &mut latest_withdrawal_bundle_event_block_hash, - *event_block_hash, - event, - )?; - } + Ok(ConnectContext { + block_height, + latest_deposit_block_hash: None, + latest_withdrawal_bundle_event_block_hash: None, + accumulator_diff: AccumulatorDiff::default(), + }) +} + +pub(crate) fn connect_block_info( + state: &State, + rwtxn: &mut RwTxn, + event_block_hash: bitcoin::BlockHash, + block_info: &BlockInfo, + context: &mut ConnectContext, +) -> Result<(), Error> { + for event in &block_info.events { + connect_2wpd_event( + state, + rwtxn, + context.block_height, + event_block_hash, + event, + context, + )?; } + Ok(()) +} + +pub(crate) fn finish_connect( + state: &State, + rwtxn: &mut RwTxn, + context: ConnectContext, +) -> Result { + let block_height = context.block_height; // Handle deposits. - if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { + if let Some(latest_deposit_block_hash) = context.latest_deposit_block_hash { let deposit_block_seq_idx = state .deposit_blocks .last(rwtxn)? @@ -564,7 +678,7 @@ pub fn connect( } // Handle withdrawals if let Some(latest_withdrawal_bundle_event_block_hash) = - latest_withdrawal_bundle_event_block_hash + context.latest_withdrawal_bundle_event_block_hash { let withdrawal_bundle_event_block_seq_idx = state .withdrawal_bundle_event_blocks @@ -604,6 +718,7 @@ pub fn connect( block_height, ) }; + state.put_withdrawal_bundle_metadata(rwtxn, &bundle.metadata())?; state.withdrawal_bundles.put( rwtxn, &m6id, @@ -615,7 +730,25 @@ pub fn connect( "Stored pending withdrawal bundle" ); } - Ok(()) + Ok(context.accumulator_diff) +} + +pub fn connect( + state: &State, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, +) -> Result { + let mut context = begin_connect(state, rwtxn)?; + for (event_block_hash, event_block_info) in &two_way_peg_data.block_info { + connect_block_info( + state, + rwtxn, + *event_block_hash, + event_block_info, + &mut context, + )?; + } + finish_connect(state, rwtxn, context) } fn disconnect_withdrawal_bundle_submitted( @@ -648,19 +781,25 @@ fn disconnect_withdrawal_bundle_submitted( WithdrawalBundleInfo::Unknown | WithdrawalBundleInfo::UnknownConfirmed { .. } => (), WithdrawalBundleInfo::Known(bundle) => { - if let Some(bundle_status) = &bundle_status - && bundle_status.latest().value - == WithdrawalBundleStatus::Pending - { - for (outpoint, output) in bundle.spend_utxos().iter().rev() { + let submitted_from_pending = + bundle_status.as_ref().is_some_and(|bundle_status| { + bundle_status.latest().value + == WithdrawalBundleStatus::Pending + }); + let submitted_from_archive = bundle_status.is_none() + && latest_bundle_status.value + == WithdrawalBundleStatus::Submitted; + if submitted_from_pending || submitted_from_archive { + for outpoint in bundle.spend_utxos().keys().rev() { let outpoint_key = OutPointKey::from_outpoint(outpoint); - if !state.stxos.delete(rwtxn, &outpoint_key)? { + if !state.unspend_utxo(rwtxn, &outpoint_key)? { return Err(Error::NoStxo { outpoint: *outpoint, }); - }; - state.utxos.put(rwtxn, &outpoint_key, output)?; + } } + } + if submitted_from_pending { state.pending_withdrawal_bundle.put(rwtxn, &(), &m6id)?; } } @@ -712,26 +851,24 @@ fn disconnect_withdrawal_bundle_confirmed( prev_bundle_status.latest().value, WithdrawalBundleStatus::SubmittedUnexpected ) { - for (outpoint, output) in bundle.spend_utxos() { + for outpoint in bundle.spend_utxos().keys() { let outpoint_key = OutPointKey::from(outpoint); - state.utxos.put(rwtxn, &outpoint_key, output)?; - if !state.stxos.delete(rwtxn, &outpoint_key)? { + if !state.unspend_utxo(rwtxn, &outpoint_key)? { return Err(Error::NoStxo { outpoint: *outpoint, }); - }; + } } } } WithdrawalBundleInfo::UnknownConfirmed { spend_utxos } => { - for (outpoint, output) in spend_utxos { + for outpoint in spend_utxos.keys() { let outpoint_key = OutPointKey::from_outpoint(outpoint); - state.utxos.put(rwtxn, &outpoint_key, output)?; - if !state.stxos.delete(rwtxn, &outpoint_key)? { + if !state.unspend_utxo(rwtxn, &outpoint_key)? { return Err(Error::NoStxo { outpoint: *outpoint, }); - }; + } } bundle = WithdrawalBundleInfo::Unknown; } @@ -756,7 +893,11 @@ fn disconnect_withdrawal_bundle_failed( .try_get(rwtxn, &m6id)? .ok_or_else(|| Error::UnknownWithdrawalBundle { m6id })?; let (prev_bundle_status, latest_bundle_status) = bundle_status.pop(); - if latest_bundle_status.value == WithdrawalBundleStatus::Submitted { + if matches!( + latest_bundle_status.value, + WithdrawalBundleStatus::Submitted + | WithdrawalBundleStatus::SubmittedUnexpected + ) { // Already applied return Ok(()); } else { @@ -781,18 +922,13 @@ fn disconnect_withdrawal_bundle_failed( break 'known; } for (outpoint, output) in bundle.spend_utxos().iter().rev() { - let outpoint_key = OutPointKey::from_outpoint(outpoint); - let spent_output = SpentOutput { - output: output.clone(), - inpoint: InPoint::Withdrawal { m6id }, - }; - state.stxos.put(rwtxn, &outpoint_key, &spent_output)?; - if !state.utxos.delete(rwtxn, &outpoint_key)? { + if !spend_withdrawal_utxo(state, rwtxn, m6id, outpoint, output)? + { return Err(error::NoUtxo { outpoint: *outpoint, } .into()); - }; + } } let (prev_latest_failed_m6id, latest_failed_m6id) = state .latest_failed_withdrawal_bundle @@ -820,6 +956,22 @@ fn disconnect_withdrawal_bundle_failed( Ok(()) } +fn spend_withdrawal_utxo( + state: &State, + rwtxn: &mut RwTxn, + m6id: M6id, + outpoint: &OutPoint, + output: &FilledOutput, +) -> Result { + let inpoint = InPoint::Withdrawal { m6id }; + let outpoint_key = OutPointKey::from_outpoint(outpoint); + let spent_output = SpentOutput { + output: output.clone(), + inpoint, + }; + state.spend_utxo(rwtxn, outpoint_key, spent_output) +} + fn disconnect_withdrawal_bundle_event( state: &State, rwtxn: &mut RwTxn, @@ -867,7 +1019,9 @@ fn disconnect_event( BlockEvent::Deposit(deposit) => { let outpoint = OutPoint::Deposit(deposit.outpoint); let outpoint_key = OutPointKey::from_outpoint(&outpoint); - if !state.utxos.delete(rwtxn, &outpoint_key)? { + let address_key = + AddressOutPointKey::new(deposit.output.address, outpoint_key); + if !state.delete_utxo(rwtxn, &address_key)? { return Err(error::NoUtxo { outpoint }.into()); } // Blocks are iterated in reverse here, so the first event block @@ -894,35 +1048,86 @@ fn disconnect_event( Ok(()) } -pub fn disconnect( +pub(crate) struct DisconnectContext { + block_height: u32, + latest_deposit_block_hash: Option, + latest_withdrawal_bundle_event_block_hash: Option, +} + +pub(crate) fn begin_disconnect( state: &State, rwtxn: &mut RwTxn, - two_way_peg_data: &TwoWayPegData, -) -> Result<(), Error> { +) -> Result { let block_height = state .try_get_height(rwtxn)? .expect("Height should not be None"); - let mut latest_deposit_block_hash = None; - let mut latest_withdrawal_bundle_event_block_hash = None; - // Restore pending withdrawal bundle - for (event_block_hash, event_block_info) in - two_way_peg_data.block_info.iter().rev() + // `connect` creates a new pending bundle after applying L1 events, so undo + // that bundle before reversing events which may restore the prior pending + // bundle. + let last_withdrawal_bundle_failure_height = state + .get_latest_failed_withdrawal_bundle(rwtxn)? + .map(|(height, _bundle)| height) + .unwrap_or_default(); + if block_height - last_withdrawal_bundle_failure_height + >= WITHDRAWAL_BUNDLE_FAILURE_GAP + && let Some(bundle_m6id) = + state.pending_withdrawal_bundle.try_get(rwtxn, &())? + && let (bundle, bundle_status) = state + .withdrawal_bundles + .try_get(rwtxn, &bundle_m6id)? + .ok_or(error::PendingWithdrawalBundleUnknown(bundle_m6id))? + && bundle_status.latest().height == block_height { - for event in event_block_info.events.iter().rev() { - let () = disconnect_event( - state, + state.pending_withdrawal_bundle.delete(rwtxn, &())?; + if let (Some(bundle_status), _latest_bundle_status) = + bundle_status.pop() + { + state.withdrawal_bundles.put( rwtxn, - block_height, - &mut latest_deposit_block_hash, - &mut latest_withdrawal_bundle_event_block_hash, - *event_block_hash, - event, + &bundle_m6id, + &(bundle, bundle_status), )?; + } else { + state.withdrawal_bundles.delete(rwtxn, &bundle_m6id)?; } } + Ok(DisconnectContext { + block_height, + latest_deposit_block_hash: None, + latest_withdrawal_bundle_event_block_hash: None, + }) +} + +pub(crate) fn disconnect_block_info( + state: &State, + rwtxn: &mut RwTxn, + event_block_hash: bitcoin::BlockHash, + block_info: &BlockInfo, + context: &mut DisconnectContext, +) -> Result<(), Error> { + for event in block_info.events.iter().rev() { + disconnect_event( + state, + rwtxn, + context.block_height, + &mut context.latest_deposit_block_hash, + &mut context.latest_withdrawal_bundle_event_block_hash, + event_block_hash, + event, + )?; + } + Ok(()) +} + +pub(crate) fn finish_disconnect( + state: &State, + rwtxn: &mut RwTxn, + context: DisconnectContext, +) -> Result<(), Error> { + let block_height = context.block_height; // Handle withdrawals if let Some(latest_withdrawal_bundle_event_block_hash) = - latest_withdrawal_bundle_event_block_hash + context.latest_withdrawal_bundle_event_block_hash { let ( last_withdrawal_bundle_event_block_seq_idx, @@ -946,35 +1151,8 @@ pub fn disconnect( return Err(Error::NoWithdrawalBundleEventBlock); }; } - let last_withdrawal_bundle_failure_height = state - .get_latest_failed_withdrawal_bundle(rwtxn)? - .map(|(height, _bundle)| height) - .unwrap_or_default(); - if block_height - last_withdrawal_bundle_failure_height - >= WITHDRAWAL_BUNDLE_FAILURE_GAP - && let Some(bundle_m6id) = - state.pending_withdrawal_bundle.try_get(rwtxn, &())? - && let (bundle, bundle_status) = state - .withdrawal_bundles - .try_get(rwtxn, &bundle_m6id)? - .ok_or(error::PendingWithdrawalBundleUnknown(bundle_m6id))? - && bundle_status.latest().height == block_height - { - state.pending_withdrawal_bundle.delete(rwtxn, &())?; - if let (Some(bundle_status), _latest_bundle_status) = - bundle_status.pop() - { - state.withdrawal_bundles.put( - rwtxn, - &bundle_m6id, - &(bundle, bundle_status), - )?; - } else { - state.withdrawal_bundles.delete(rwtxn, &bundle_m6id)?; - } - } // Handle deposits - if let Some(latest_deposit_block_hash) = latest_deposit_block_hash { + if let Some(latest_deposit_block_hash) = context.latest_deposit_block_hash { let ( last_deposit_block_seq_idx, (last_deposit_block_hash, last_deposit_block_height), @@ -994,6 +1172,28 @@ pub fn disconnect( Ok(()) } +pub fn disconnect( + state: &State, + rwtxn: &mut RwTxn, + two_way_peg_data: &TwoWayPegData, +) -> Result<(), Error> { + let mut context = begin_disconnect(state, rwtxn)?; + // Restore pending withdrawal bundle. Disconnect L1 blocks and their + // events newest-to-oldest, matching the previous whole-batch traversal. + for (event_block_hash, event_block_info) in + two_way_peg_data.block_info.iter().rev() + { + disconnect_block_info( + state, + rwtxn, + *event_block_hash, + event_block_info, + &mut context, + )?; + } + finish_disconnect(state, rwtxn, context) +} + #[cfg(test)] mod test { use std::collections::BTreeMap; @@ -1007,22 +1207,197 @@ mod test { use crate::{ state::{ - HeightStamped, RollBack, State, WithdrawalBundleInfo, + Error, HeightStamped, RollBack, State, WithdrawalBundleInfo, test::{bitcoin_filled_output, fresh_state}, two_way_peg_data::{ - collect_withdrawal_bundle, connect, disconnect, + collect_withdrawal_bundle, + collect_withdrawal_bundle_with_max_inputs, connect, + connect_withdrawal_bundle_failed, + connect_withdrawal_bundle_submitted, disconnect, disconnect_withdrawal_bundle_failed, + disconnect_withdrawal_bundle_submitted, }, }, types::{ - Address, FilledOutput, FilledOutputContent, InPoint, M6id, - OutPoint, OutPointKey, Txid, WithdrawalBundle, - WithdrawalBundleEvent, WithdrawalBundleEventStatus, - WithdrawalBundleStatus, WithdrawalOutputContent, + AccumulatorDiff, Address, FilledOutput, FilledOutputContent, + InPoint, M6id, OutPoint, OutPointKey, SpentOutput, Txid, + WithdrawalBundle, WithdrawalBundleEvent, + WithdrawalBundleEventStatus, WithdrawalBundleStatus, + WithdrawalOutputContent, proto::mainchain::{BlockEvent, BlockInfo, Deposit, TwoWayPegData}, }, }; + #[test] + fn unknown_submission_blocks_without_mutating_state() -> anyhow::Result<()> + { + let (_temp_dir, env, state) = + fresh_state("unknown_submission_blocks_without_mutating_state")?; + let event_block_hash = bitcoin::BlockHash::from_byte_array([2; 32]); + let m6id = M6id(bitcoin::Txid::from_byte_array([3; 32])); + let mut rwtxn = env.write_txn()?; + let mut accumulator_diff = AccumulatorDiff::default(); + + let err = connect_withdrawal_bundle_submitted( + &state, + &mut rwtxn, + 1, + &event_block_hash, + m6id, + &mut accumulator_diff, + ) + .unwrap_err(); + + assert!(matches!( + err, + crate::state::error::ConnectWithdrawalBundleSubmitted::MissingWithdrawalBundleMetadata { + event_block_hash: actual_event_block_hash, + m6id: actual_m6id, + } if actual_event_block_hash == event_block_hash && actual_m6id == m6id + )); + assert!(accumulator_diff.is_empty()); + assert!(state.withdrawal_bundles.try_get(&rwtxn, &m6id)?.is_none()); + Ok(()) + } + + #[test] + fn archived_submission_metadata_survives_disconnect_and_reapplies() + -> anyhow::Result<()> { + let (_temp_dir, env, state) = fresh_state( + "archived_submission_metadata_survives_disconnect_and_reapplies", + )?; + let outpoint = OutPoint::Regular { + txid: Txid::from([1; 32]), + vout: 0, + }; + let outpoint_key = OutPointKey::from(&outpoint); + let output = bitcoin_filled_output(Address::ALL_ZEROS, 1_000); + let mut spend_utxos = BTreeMap::new(); + spend_utxos.insert(outpoint, output.clone()); + let bundle = WithdrawalBundle::new( + 7, + bitcoin::Amount::ZERO, + spend_utxos, + Vec::new(), + )?; + let m6id = bundle.compute_m6id(); + let event_block_hash = bitcoin::BlockHash::from_byte_array([2; 32]); + + let mut rwtxn = env.write_txn()?; + state.put_utxo(&mut rwtxn, outpoint_key, output.clone(), 0)?; + state.put_withdrawal_bundle_metadata(&mut rwtxn, &bundle.metadata())?; + rwtxn.commit()?; + + let mut rwtxn = env.write_txn()?; + let mut accumulator_diff = AccumulatorDiff::default(); + connect_withdrawal_bundle_submitted( + &state, + &mut rwtxn, + 1, + &event_block_hash, + m6id, + &mut accumulator_diff, + )?; + assert_eq!(accumulator_diff.counts(), (0, 1)); + assert!(state.utxos.try_get(&rwtxn, &outpoint_key)?.is_none()); + assert_eq!( + state.stxos.get(&rwtxn, &outpoint_key)?.output.inpoint, + InPoint::Withdrawal { m6id } + ); + rwtxn.commit()?; + + let mut rwtxn = env.write_txn()?; + disconnect_withdrawal_bundle_submitted(&state, &mut rwtxn, 1, m6id)?; + assert!(state.utxos.try_get(&rwtxn, &outpoint_key)?.is_some()); + assert!(state.withdrawal_bundles.try_get(&rwtxn, &m6id)?.is_none()); + assert!( + state + .try_get_withdrawal_bundle_metadata(&rwtxn, m6id)? + .is_some() + ); + rwtxn.commit()?; + + let mut rwtxn = env.write_txn()?; + let mut reapplied_diff = AccumulatorDiff::default(); + connect_withdrawal_bundle_submitted( + &state, + &mut rwtxn, + 1, + &event_block_hash, + m6id, + &mut reapplied_diff, + )?; + assert_eq!(reapplied_diff.counts(), (0, 1)); + Ok(()) + } + + #[test] + fn submitted_then_failed_bundle_has_no_net_accumulator_diff() + -> anyhow::Result<()> { + let (_temp_dir, env, state) = fresh_state( + "submitted_then_failed_bundle_has_no_net_accumulator_diff", + )?; + let outpoint = OutPoint::Regular { + txid: Txid::from([1; 32]), + vout: 0, + }; + let output = bitcoin_filled_output(Address::ALL_ZEROS, 1_000); + let mut spend_utxos = BTreeMap::new(); + spend_utxos.insert(outpoint, output.clone()); + let bundle = WithdrawalBundle::new( + 1, + bitcoin::Amount::ZERO, + spend_utxos, + Vec::new(), + )?; + let m6id = bundle.compute_m6id(); + let event_block_hash = bitcoin::BlockHash::from_byte_array([2; 32]); + + let mut rwtxn = env.write_txn()?; + state.put_utxo(&mut rwtxn, OutPointKey::from(&outpoint), output, 0)?; + state.withdrawal_bundles.put( + &mut rwtxn, + &m6id, + &( + WithdrawalBundleInfo::Known(bundle), + RollBack::>::new( + WithdrawalBundleStatus::Pending, + 0, + ), + ), + )?; + state + .pending_withdrawal_bundle + .put(&mut rwtxn, &(), &m6id)?; + + let mut accumulator_diff = AccumulatorDiff::default(); + connect_withdrawal_bundle_submitted( + &state, + &mut rwtxn, + 1, + &event_block_hash, + m6id, + &mut accumulator_diff, + )?; + anyhow::ensure!(accumulator_diff.counts() == (0, 1)); + connect_withdrawal_bundle_failed( + &state, + &mut rwtxn, + 2, + m6id, + &mut accumulator_diff, + )?; + + anyhow::ensure!(accumulator_diff.is_empty()); + anyhow::ensure!( + state + .utxos + .try_get(&rwtxn, &OutPointKey::from(&outpoint))? + .is_some() + ); + Ok(()) + } + // a failed known bundle reinstates its utxos as spendable, so disconnecting // the failure must spend them again #[test] @@ -1065,7 +1440,7 @@ mod test { &RollBack::>::new(m6id, 1), )?; // the failure reinstated the utxo - state.utxos.put(&mut rwtxn, &key, &output)?; + state.put_utxo(&mut rwtxn, key, output, 1)?; rwtxn.commit()?; m6id }; @@ -1074,7 +1449,42 @@ mod test { disconnect_withdrawal_bundle_failed(&state, &mut rwtxn, 1, m6id)?; anyhow::ensure!(state.utxos.try_get(&rwtxn, &key)?.is_none()); let stxo = state.stxos.get(&rwtxn, &key)?; - anyhow::ensure!(stxo.inpoint == InPoint::Withdrawal { m6id }); + anyhow::ensure!(stxo.output.inpoint == InPoint::Withdrawal { m6id }); + Ok(()) + } + + #[test] + fn duplicate_failed_event_after_unexpected_submission_disconnects_cleanly() + -> anyhow::Result<()> { + let (_temp_dir, env, state) = fresh_state( + "duplicate_failed_event_after_unexpected_submission_disconnects_cleanly", + )?; + let m6id = M6id(bitcoin::Txid::from_byte_array([8; 32])); + let mut status = RollBack::>::new( + WithdrawalBundleStatus::SubmittedUnexpected, + 1, + ); + status + .push(WithdrawalBundleStatus::Failed, 2) + .expect("submitted-unexpected to failed is valid"); + let mut rwtxn = env.write_txn()?; + state.withdrawal_bundles.put( + &mut rwtxn, + &m6id, + &(WithdrawalBundleInfo::Unknown, status), + )?; + + // The first reverse applies the failure; a duplicate older failure is + // already undone once SubmittedUnexpected is visible again. + disconnect_withdrawal_bundle_failed(&state, &mut rwtxn, 2, m6id)?; + disconnect_withdrawal_bundle_failed(&state, &mut rwtxn, 2, m6id)?; + + let (_, status) = state.withdrawal_bundles.get(&rwtxn, &m6id)?; + assert_eq!( + status.latest().value, + WithdrawalBundleStatus::SubmittedUnexpected + ); + assert_eq!(status.latest().height, 1); Ok(()) } @@ -1208,10 +1618,11 @@ mod test { ), memo: Vec::new(), }; - state.utxos.put( + state.put_utxo( &mut rwtxn, - &OutPointKey::from(&outpoint), - &output, + OutPointKey::from(&outpoint), + output, + crate::state::WITHDRAWAL_BUNDLE_FAILURE_GAP, )?; } f(&state, &mut rwtxn) @@ -1219,6 +1630,209 @@ mod test { Ok(res) } + #[test] + fn pending_bundle_metadata_outlives_its_l2_branch() -> anyhow::Result<()> { + with_state_with_withdrawals( + "pending_bundle_metadata_outlives_its_l2_branch", + 1, + regtest_p2wpkh_address, + |state, rwtxn| -> anyhow::Result<()> { + let two_way_peg_data = TwoWayPegData::default(); + connect(state, rwtxn, &two_way_peg_data)?; + let m6id = state.pending_withdrawal_bundle.get(rwtxn, &())?; + assert!( + state + .try_get_withdrawal_bundle_metadata(rwtxn, m6id)? + .is_some() + ); + + disconnect(state, rwtxn, &two_way_peg_data)?; + assert!( + state + .pending_withdrawal_bundle + .try_get(rwtxn, &())? + .is_none() + ); + assert!( + state.withdrawal_bundles.try_get(rwtxn, &m6id)?.is_none() + ); + assert!( + state + .try_get_withdrawal_bundle_metadata(rwtxn, m6id)? + .is_some() + ); + Ok(()) + }, + )??; + Ok(()) + } + + #[test] + fn disconnect_removes_new_pending_before_restoring_submitted_pending() + -> anyhow::Result<()> { + with_state_with_withdrawals( + "disconnect_removes_new_pending_before_restoring_submitted_pending", + 1, + regtest_p2wpkh_address, + |state, rwtxn| -> anyhow::Result<()> { + connect(state, rwtxn, &TwoWayPegData::default())?; + let submitted_m6id = + state.pending_withdrawal_bundle.get(rwtxn, &())?; + + let block_height = + crate::state::WITHDRAWAL_BUNDLE_FAILURE_GAP + 1; + state.height.put(rwtxn, &(), &block_height)?; + let next_outpoint = OutPoint::Regular { + txid: Txid::from([2; 32]), + vout: 0, + }; + let next_output = FilledOutput { + address: Address::ALL_ZEROS, + content: FilledOutputContent::BitcoinWithdrawal( + WithdrawalOutputContent { + value: bitcoin::Amount::from_sat(1_000), + main_fee: bitcoin::Amount::ZERO, + main_address: regtest_p2wpkh_address(2), + }, + ), + memo: Vec::new(), + }; + state.put_utxo( + rwtxn, + OutPointKey::from(&next_outpoint), + next_output, + block_height, + )?; + + let event_block_hash = + bitcoin::BlockHash::from_byte_array([3; 32]); + let mut two_way_peg_data = TwoWayPegData::default(); + two_way_peg_data.block_info.insert( + event_block_hash, + BlockInfo { + bmm_commitment: None, + events: vec![BlockEvent::WithdrawalBundle( + WithdrawalBundleEvent { + m6id: submitted_m6id, + status: WithdrawalBundleEventStatus::Submitted, + }, + )], + }, + ); + connect(state, rwtxn, &two_way_peg_data)?; + let new_pending_m6id = + state.pending_withdrawal_bundle.get(rwtxn, &())?; + anyhow::ensure!(new_pending_m6id != submitted_m6id); + + disconnect(state, rwtxn, &two_way_peg_data)?; + + anyhow::ensure!( + state.pending_withdrawal_bundle.get(rwtxn, &())? + == submitted_m6id + ); + let (_bundle, submitted_status) = + state.withdrawal_bundles.get(rwtxn, &submitted_m6id)?; + anyhow::ensure!( + submitted_status.latest().value + == WithdrawalBundleStatus::Pending + ); + anyhow::ensure!( + submitted_status.latest().height + == crate::state::WITHDRAWAL_BUNDLE_FAILURE_GAP + ); + anyhow::ensure!( + state + .withdrawal_bundles + .try_get(rwtxn, &new_pending_m6id)? + .is_none() + ); + anyhow::ensure!( + state + .try_get_withdrawal_bundle_metadata( + rwtxn, + new_pending_m6id, + )? + .is_some() + ); + Ok(()) + }, + )??; + Ok(()) + } + + #[test] + fn withdrawal_bundle_input_count_is_bounded() -> anyhow::Result<()> { + let bundle = with_state_with_withdrawals( + "withdrawal_bundle_input_count_is_bounded", + 3, + regtest_p2wpkh_address, + |state, rwtxn| { + collect_withdrawal_bundle_with_max_inputs(state, rwtxn, 42, 2) + }, + )?? + .ok_or_else(|| anyhow::anyhow!("expected a withdrawal bundle"))?; + + assert_eq!(bundle.spend_utxos().len(), 2); + for idx in 1_u32..=2 { + let mut txid = [0_u8; 32]; + txid[28..].copy_from_slice(&idx.to_be_bytes()); + assert!(bundle.spend_utxos().contains_key(&OutPoint::Regular { + txid: txid.into(), + vout: 0, + })); + } + let mut omitted_txid = [0_u8; 32]; + omitted_txid[28..].copy_from_slice(&3_u32.to_be_bytes()); + assert!(!bundle.spend_utxos().contains_key(&OutPoint::Regular { + txid: omitted_txid.into(), + vout: 0, + })); + assert!(bundle.has_valid_inputs_commitment()); + assert!(bundle.metadata().is_within_size_limit()); + Ok(()) + } + + #[test] + fn withdrawal_bundle_limit_is_applied_before_aggregation() + -> anyhow::Result<()> { + let (_temp_dir, env, state) = fresh_state( + "withdrawal_bundle_limit_is_applied_before_aggregation", + )?; + let main_address = regtest_p2wpkh_address(1); + let mut rwtxn = env.write_txn()?; + for (marker, value) in [(1_u8, 1_u64), (2, u64::MAX)] { + let outpoint = OutPoint::Regular { + txid: Txid::from([marker; 32]), + vout: 0, + }; + let output = FilledOutput { + address: Address::ALL_ZEROS, + content: FilledOutputContent::BitcoinWithdrawal( + WithdrawalOutputContent { + value: bitcoin::Amount::from_sat(value), + main_fee: bitcoin::Amount::ZERO, + main_address: main_address.clone(), + }, + ), + memo: Vec::new(), + }; + state.put_utxo( + &mut rwtxn, + OutPointKey::from(&outpoint), + output, + 0, + )?; + } + + let bundle = collect_withdrawal_bundle_with_max_inputs( + &state, &rwtxn, 42, 1, + )? + .ok_or_else(|| anyhow::anyhow!("expected a withdrawal bundle"))?; + assert_eq!(bundle.spend_utxos().len(), 1); + assert_eq!(bundle.tx().output[2].value.to_sat(), 1); + Ok(()) + } + #[test] fn collect_withdrawal_bundle_p2wpkh_off_by_one_does_not_exceed_weight() -> anyhow::Result<()> { @@ -1250,6 +1864,103 @@ mod test { Ok(()) } + fn deposit_two_way_peg_data( + event_block_hash: bitcoin::BlockHash, + outpoint: bitcoin::OutPoint, + output: FilledOutput, + ) -> TwoWayPegData { + let mut block_info = LinkedHashMap::new(); + block_info.insert( + event_block_hash, + BlockInfo { + bmm_commitment: None, + events: vec![BlockEvent::Deposit(Deposit { + tx_index: 0, + outpoint, + output, + })], + }, + ); + TwoWayPegData { block_info } + } + + #[test] + fn duplicate_deposit_cannot_overwrite_utxo() -> anyhow::Result<()> { + let (_temp_dir, env, state) = + fresh_state("duplicate_deposit_cannot_overwrite_utxo")?; + let main_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([1; 32]), + vout: 0, + }; + let outpoint = OutPoint::Deposit(main_outpoint); + let key = OutPointKey::from(&outpoint); + let existing_output = bitcoin_filled_output(Address([1; 20]), 1_000); + let deposit_data = deposit_two_way_peg_data( + bitcoin::BlockHash::from_byte_array([2; 32]), + main_outpoint, + bitcoin_filled_output(Address([2; 20]), 2_000), + ); + + let mut rwtxn = env.write_txn()?; + state.height.put(&mut rwtxn, &(), &1)?; + state.put_utxo(&mut rwtxn, key, existing_output.clone(), 0)?; + + let err = connect(&state, &mut rwtxn, &deposit_data).unwrap_err(); + anyhow::ensure!(matches!( + err, + Error::DuplicateDeposit { + outpoint: duplicate + } if duplicate == outpoint + )); + anyhow::ensure!(state.utxos.get(&rwtxn, &key)? == existing_output); + anyhow::ensure!(state.stxos.try_get(&rwtxn, &key)?.is_none()); + anyhow::ensure!(state.deposit_blocks.last(&rwtxn)?.is_none()); + Ok(()) + } + + #[test] + fn duplicate_deposit_cannot_resurrect_stxo() -> anyhow::Result<()> { + let (_temp_dir, env, state) = + fresh_state("duplicate_deposit_cannot_resurrect_stxo")?; + let main_outpoint = bitcoin::OutPoint { + txid: bitcoin::Txid::from_byte_array([3; 32]), + vout: 1, + }; + let outpoint = OutPoint::Deposit(main_outpoint); + let key = OutPointKey::from(&outpoint); + let existing_output = bitcoin_filled_output(Address([3; 20]), 3_000); + let spent_output = SpentOutput { + output: existing_output.clone(), + inpoint: InPoint::Regular { + txid: Txid::from([4; 32]), + vin: 0, + }, + }; + let deposit_data = deposit_two_way_peg_data( + bitcoin::BlockHash::from_byte_array([5; 32]), + main_outpoint, + bitcoin_filled_output(Address([5; 20]), 5_000), + ); + + let mut rwtxn = env.write_txn()?; + state.height.put(&mut rwtxn, &(), &1)?; + state.put_utxo(&mut rwtxn, key, existing_output, 0)?; + anyhow::ensure!(state.spend_utxo(&mut rwtxn, key, spent_output)?); + let existing_stxo = state.stxos.get(&rwtxn, &key)?; + + let err = connect(&state, &mut rwtxn, &deposit_data).unwrap_err(); + anyhow::ensure!(matches!( + err, + Error::DuplicateDeposit { + outpoint: duplicate + } if duplicate == outpoint + )); + anyhow::ensure!(state.utxos.try_get(&rwtxn, &key)?.is_none()); + anyhow::ensure!(state.stxos.get(&rwtxn, &key)? == existing_stxo); + anyhow::ensure!(state.deposit_blocks.last(&rwtxn)?.is_none()); + Ok(()) + } + // connecting a deposit then disconnecting it on a reorg must round-trip #[test] fn deposit_reorg_round_trips() -> anyhow::Result<()> { @@ -1375,7 +2086,8 @@ mod test { block_info.insert(h2, b2); let tdp = TwoWayPegData { block_info }; - let () = connect(&state, &mut rwtxn, &tdp)?; + let accumulator_diff = connect(&state, &mut rwtxn, &tdp)?; + anyhow::ensure!(accumulator_diff.counts() == (2, 0)); anyhow::ensure!(state.utxos.len(&rwtxn)? == 2); disconnect(&state, &mut rwtxn, &tdp)?; diff --git a/lib/types/address.rs b/lib/types/address.rs index b43c901b..baa069e8 100644 --- a/lib/types/address.rs +++ b/lib/types/address.rs @@ -7,11 +7,13 @@ use utoipa::ToSchema; use crate::types::THIS_SIDECHAIN; +pub const ADDRESS_SIZE: usize = 20; + #[derive(Debug, Error)] pub enum AddressParseError { #[error("bs58 error")] Bs58(#[from] bitcoin::base58::InvalidCharacterError), - #[error("wrong address length {0} != 20")] + #[error("wrong address length {0} != {ADDRESS_SIZE}")] WrongLength(usize), } @@ -20,10 +22,10 @@ pub enum AddressParseError { )] #[repr(transparent)] #[schema(value_type = String)] -pub struct Address(pub [u8; 20]); +pub struct Address(pub [u8; ADDRESS_SIZE]); impl Address { - pub const ALL_ZEROS: Self = Self([0; 20]); + pub const ALL_ZEROS: Self = Self([0; ADDRESS_SIZE]); pub fn as_base58(&self) -> String { bitcoin::base58::encode(&self.0) @@ -50,8 +52,8 @@ impl std::fmt::Debug for Address { } } -impl From<[u8; 20]> for Address { - fn from(other: [u8; 20]) -> Self { +impl From<[u8; ADDRESS_SIZE]> for Address { + fn from(other: [u8; ADDRESS_SIZE]) -> Self { Self(other) } } @@ -74,7 +76,8 @@ impl<'de> Deserialize<'de> for Address { if deserializer.is_human_readable() { DisplayFromStr::deserialize_as(deserializer) } else { - <[u8; 20] as Deserialize>::deserialize(deserializer).map(Self) + <[u8; ADDRESS_SIZE] as Deserialize>::deserialize(deserializer) + .map(Self) } } } diff --git a/lib/types/hashes.rs b/lib/types/hashes.rs index 18b87b38..8b1aa0dc 100644 --- a/lib/types/hashes.rs +++ b/lib/types/hashes.rs @@ -147,6 +147,34 @@ impl utoipa::ToSchema for MerkleRoot { } } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MerkleProof { + pub leaf_index: usize, + pub siblings: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub struct MerkleProofNode { + pub hash: Hash, + pub is_left: bool, +} + +impl MerkleProof { + pub fn verify(&self, leaf: Hash, root: MerkleRoot) -> bool { + let mut hash = leaf; + + for sibling in &self.siblings { + hash = if sibling.is_left { + hash_with_scratch_buffer(&(sibling.hash, hash)) + } else { + hash_with_scratch_buffer(&(hash, sibling.hash)) + }; + } + + hash == Hash::from(root) + } +} + #[derive( BorshDeserialize, BorshSerialize, diff --git a/lib/types/mod.rs b/lib/types/mod.rs index d53e024b..3fd3bdd2 100644 --- a/lib/types/mod.rs +++ b/lib/types/mod.rs @@ -1,13 +1,18 @@ use std::{ cmp::Ordering, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, sync::LazyLock, }; use bitcoin::amount::CheckedSum as _; -use borsh::BorshSerialize; + +use hashlink::{LinkedHashMap, linked_hash_map::Entry}; +use rustreexo::accumulator::{ + mem_forest::MemForest, node_hash::AccumulatorHash, proof::Proof, +}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; +use std::io; use thiserror::Error; use utoipa::ToSchema; @@ -21,20 +26,20 @@ pub mod proto; pub mod schema; pub mod transaction; -pub use address::Address; +pub use address::{ADDRESS_SIZE, Address}; pub use bitasset_data::{BitAssetData, BitAssetDataUpdates, Update}; pub use hashes::{ - AssetId, BitAssetId, BlockHash, DutchAuctionId, Hash, M6id, MerkleRoot, - Txid, + AssetId, BitAssetId, BlockHash, DutchAuctionId, Hash, M6id, MerkleProof, + MerkleProofNode, MerkleRoot, Txid, }; pub use keys::{EncryptionPubKey, VerifyingKey}; pub use transaction::{ - AmmBurn, AmmMint, AmmSwap, AssetOutput, AssetOutputContent, Authorized, - AuthorizedTransaction, BitcoinOutput, BitcoinOutputContent, - DutchAuctionBid, DutchAuctionCollect, DutchAuctionParams, FilledOutput, - FilledOutputContent, FilledTransaction, InPoint, OutPoint, OutPointKey, - Output, OutputContent, PointedOutput, SpentOutput, Transaction, TxData, - TxInputs, WithdrawalOutputContent, + AddressOutPointKey, AddressTxidKey, AmmBurn, AmmMint, AmmSwap, AssetOutput, + AssetOutputContent, Authorized, AuthorizedTransaction, BitcoinOutput, + BitcoinOutputContent, DutchAuctionBid, DutchAuctionCollect, + DutchAuctionParams, FilledOutput, FilledOutputContent, FilledTransaction, + InPoint, OutPoint, OutPointKey, Output, OutputContent, PointedOutput, + SpentOutput, Transaction, TxData, TxInputs, WithdrawalOutputContent, }; pub const THIS_SIDECHAIN: u8 = 4; @@ -147,7 +152,7 @@ where } #[derive( - BorshSerialize, + borsh::BorshSerialize, Clone, Debug, Deserialize, @@ -213,6 +218,8 @@ pub static OP_DRIVECHAIN_SCRIPT: LazyLock = enum WithdrawalBundleErrorInner { #[error("bundle too heavy: weight `{weight}` > max weight `{max_weight}`")] BundleTooHeavy { weight: u64, max_weight: u64 }, + #[error("too many bundle inputs: `{inputs}` > max inputs `{max_inputs}`")] + TooManyInputs { inputs: usize, max_inputs: usize }, } #[derive(Debug, Error)] @@ -222,6 +229,7 @@ pub struct WithdrawalBundleError(#[from] WithdrawalBundleErrorInner); #[serde_as] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] pub struct WithdrawalBundle { + creation_height: u32, #[schema(value_type = Vec<( transaction::OutPoint, transaction::FilledOutput)> @@ -232,7 +240,46 @@ pub struct WithdrawalBundle { tx: bitcoin::Transaction, } +/// Branch-independent information needed to identify and reapply a withdrawal +/// bundle. Output values are deliberately omitted: they are loaded from local +/// state before the committed outpoints are spent. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct WithdrawalBundleMetadata { + creation_height: u32, + spend_outpoints: BTreeSet, + tx: bitcoin::Transaction, +} + impl WithdrawalBundle { + /// Bound bundle metadata independently of the number of withdrawal UTXOs + /// accumulated in the state. Remaining UTXOs are collected by later + /// bundles. + pub const MAX_INPUTS: usize = 1 << 16; + + fn inputs_commitment_txout( + block_height: u32, + spend_outpoints: I, + ) -> bitcoin::TxOut + where + I: IntoIterator, + { + let mut inputs: Vec = spend_outpoints.into_iter().collect(); + // Commit to the sidechain height at which the bundle was built. + inputs.push(OutPoint::Regular { + txid: [0; 32].into(), + vout: block_height, + }); + let commitment = hashes::hash_with_scratch_buffer(&inputs); + let script_pubkey = bitcoin::script::Builder::new() + .push_opcode(bitcoin::opcodes::all::OP_RETURN) + .push_slice(commitment) + .into_script(); + bitcoin::TxOut { + value: bitcoin::Amount::ZERO, + script_pubkey, + } + } + /// Compute the size of a single txout pub const fn txout_size(spk_size: u32) -> Option { let Some(size) = (bitcoin::Amount::SIZE as u32) @@ -360,28 +407,16 @@ impl WithdrawalBundle { spend_utxos: BTreeMap, bundle_outputs: Vec, ) -> Result { - let inputs_commitment_txout = { - // Create inputs commitment. - let inputs: Vec = [ - // Commit to inputs. - spend_utxos.keys().copied().collect(), - // Commit to block height. - vec![OutPoint::Regular { - txid: [0; 32].into(), - vout: block_height, - }], - ] - .concat(); - let commitment = hashes::hash_with_scratch_buffer(&inputs); - let script_pubkey = bitcoin::script::Builder::new() - .push_opcode(bitcoin::opcodes::all::OP_RETURN) - .push_slice(commitment) - .into_script(); - bitcoin::TxOut { - value: bitcoin::Amount::ZERO, - script_pubkey, - } - }; + if spend_utxos.len() > Self::MAX_INPUTS { + Err(WithdrawalBundleErrorInner::TooManyInputs { + inputs: spend_utxos.len(), + max_inputs: Self::MAX_INPUTS, + })?; + } + let inputs_commitment_txout = Self::inputs_commitment_txout( + block_height, + spend_utxos.keys().copied(), + ); let mainchain_fee_txout = { let script_pubkey = bitcoin::script::Builder::new() .push_opcode(bitcoin::opcodes::all::OP_RETURN) @@ -410,7 +445,11 @@ impl WithdrawalBundle { max_weight: bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64, })?; } - Ok(Self { spend_utxos, tx }) + Ok(Self { + creation_height: block_height, + spend_utxos, + tx, + }) } pub fn compute_m6id(&self) -> M6id { @@ -421,11 +460,73 @@ impl WithdrawalBundle { &self.spend_utxos } + pub fn metadata(&self) -> WithdrawalBundleMetadata { + WithdrawalBundleMetadata { + creation_height: self.creation_height, + spend_outpoints: self.spend_utxos.keys().copied().collect(), + tx: self.tx.clone(), + } + } + + /// Check that the archived input outpoints and creation height match the + /// commitment carried by the withdrawal transaction. + pub fn has_valid_inputs_commitment(&self) -> bool { + self.tx.output.get(1) + == Some(&Self::inputs_commitment_txout( + self.creation_height, + self.spend_utxos.keys().copied(), + )) + } + pub fn tx(&self) -> &bitcoin::Transaction { &self.tx } } +impl WithdrawalBundleMetadata { + /// Leaves ample headroom above the maximum encoded outpoint set and + /// standard withdrawal transaction. + pub const MAX_SERIALIZED_SIZE: usize = 4 * 1024 * 1024; + + pub fn compute_m6id(&self) -> M6id { + M6id(self.tx.compute_txid()) + } + + pub fn spend_outpoints(&self) -> &BTreeSet { + &self.spend_outpoints + } + + pub fn has_valid_inputs_commitment(&self) -> bool { + self.tx.output.get(1) + == Some(&WithdrawalBundle::inputs_commitment_txout( + self.creation_height, + self.spend_outpoints.iter().copied(), + )) + } + + pub fn is_within_size_limit(&self) -> bool { + self.spend_outpoints.len() <= WithdrawalBundle::MAX_INPUTS + && bincode::serialized_size(self) + .is_ok_and(|size| size <= Self::MAX_SERIALIZED_SIZE as u64) + } + + /// Hydrate committed outpoints with values loaded from the state being + /// connected. Metadata received from a peer cannot supply output values. + pub fn with_spend_utxos( + &self, + spend_utxos: BTreeMap, + ) -> Option { + if !self.spend_outpoints.iter().eq(spend_utxos.keys()) { + return None; + } + Some(WithdrawalBundle { + creation_height: self.creation_height, + spend_utxos, + tx: self.tx.clone(), + }) + } +} + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct TwoWayPegData { pub deposits: HashMap, @@ -433,7 +534,9 @@ pub struct TwoWayPegData { pub bundle_statuses: HashMap, } -#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[derive( + borsh::BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema, +)] pub struct Body { pub coinbase: Vec, pub transactions: Vec, @@ -489,8 +592,62 @@ impl Body { coinbase: &[Output], txs: &[Transaction], ) -> MerkleRoot { - // FIXME: Compute actual merkle root instead of just a hash. - hashes::hash_with_scratch_buffer(&(coinbase, txs)).into() + let mut leaves = Vec::with_capacity(txs.len() + 1); + leaves.push(hashes::hash_with_scratch_buffer(&coinbase)); + leaves.extend(txs.iter().map(hashes::hash_with_scratch_buffer)); + Body::compute_cbmt_tree(&leaves)[0].into() + } + + // https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct + fn compute_cbmt_tree(leaves: &[Hash]) -> Vec { + let n = leaves.len(); + let mut nodes = vec![Hash::default(); 2 * n - 1]; + + nodes[n - 1..].copy_from_slice(leaves); + + for idx in (0..n - 1).rev() { + nodes[idx] = hashes::hash_with_scratch_buffer(&( + nodes[2 * idx + 1], + nodes[2 * idx + 2], + )); + } + + nodes + } + + pub fn compute_tx_merkle_proof( + coinbase: &[Output], + txs: &[Transaction], + tx_idx: usize, + ) -> MerkleProof { + let mut leaves = Vec::with_capacity(txs.len() + 1); + leaves.push(hashes::hash_with_scratch_buffer(&coinbase)); + leaves.extend(txs.iter().map(hashes::hash_with_scratch_buffer)); + Body::compute_cbmt_proof(&leaves, tx_idx + 1) + } + + fn compute_cbmt_proof(leaves: &[Hash], leaf_index: usize) -> MerkleProof { + let n = leaves.len(); + let nodes = Body::compute_cbmt_tree(leaves); + let mut idx = leaf_index + n - 1; + let mut siblings = Vec::new(); + + while idx > 0 { + let sibling_idx = (idx + 1) ^ 1; + let sibling_idx = sibling_idx - 1; + + siblings.push(MerkleProofNode { + hash: nodes[sibling_idx], + is_left: sibling_idx < idx, + }); + + idx = (idx - 1) / 2; + } + + MerkleProof { + leaf_index, + siblings, + } } pub fn get_inputs(&self) -> Vec { @@ -563,7 +720,7 @@ pub struct DisconnectData { #[derive(Eq, PartialEq, Clone, Debug)] pub struct AggregatedWithdrawal { - pub spend_utxos: HashMap, + pub spend_utxos: BTreeMap, pub main_address: bitcoin::Address, pub value: bitcoin::Amount, pub main_fee: bitcoin::Amount, @@ -590,6 +747,463 @@ impl PartialOrd for AggregatedWithdrawal { } } +/// Hash type used by the utreexo accumulator. +/// +/// `Blake3UtxoHash::Some` is a real 32-byte BLAKE3 digest. +/// `Placeholder` and `Empty` are rustreexo sentinel values used internally by +/// the accumulator. They are not real UTXO hashes and should not appear in +/// serialized accumulator diffs. +/// +/// This type stores the concrete BLAKE3 digest as `[u8; 32]` instead of +/// `blake3::Hash` because rustreexo and the surrounding accumulator code need +/// ordering traits such as `Ord`, while `blake3::Hash` intentionally does not +/// implement `Ord`. +/// +/// WARNING: the exact `parent_hash` algorithm is consensus-critical. Changing +/// it changes all accumulator roots and invalidates existing proofs. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Blake3UtxoHash { + Some([u8; 32]), + Placeholder, + #[default] + Empty, +} + +impl Blake3UtxoHash { + pub fn hash_bytes(bytes: &[u8]) -> Self { + Self::Some(*blake3::hash(bytes).as_bytes()) + } + + pub fn as_bytes(&self) -> Option<[u8; 32]> { + match self { + Self::Some(bytes) => Some(*bytes), + Self::Placeholder | Self::Empty => None, + } + } +} + +impl From for Blake3UtxoHash { + fn from(hash: blake3::Hash) -> Self { + Self::Some(*hash.as_bytes()) + } +} + +impl From<[u8; 32]> for Blake3UtxoHash { + fn from(bytes: [u8; 32]) -> Self { + Self::Some(bytes) + } +} + +impl std::fmt::Display for Blake3UtxoHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Some(bytes) => write!(f, "{:?}", bytes), + Self::Placeholder => write!(f, "Placeholder"), + Self::Empty => write!(f, "Empty"), + } + } +} + +impl AccumulatorHash for Blake3UtxoHash { + fn placeholder() -> Self { + Self::Placeholder + } + + fn empty() -> Self { + Self::Empty + } + + fn is_placeholder(&self) -> bool { + matches!(self, Self::Placeholder) + } + + fn is_empty(&self) -> bool { + matches!(self, Self::Empty) + } + + fn write(&self, writer: &mut W) -> io::Result<()> + where + W: io::Write, + { + match self { + Self::Empty => writer.write_all(&[0]), + Self::Placeholder => writer.write_all(&[1]), + Self::Some(bytes) => { + writer.write_all(&[2])?; + writer.write_all(bytes) + } + } + } + + fn read(reader: &mut R) -> io::Result + where + R: io::Read, + { + let mut tag = [0]; + reader.read_exact(&mut tag)?; + match tag { + [0] => Ok(Self::Empty), + [1] => Ok(Self::Placeholder), + [2] => { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Ok(Self::Some(bytes)) + } + [_] => Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected tag for Blake3UtxoHash", + )), + } + } + + fn parent_hash(left: &Self, right: &Self) -> Self { + let (Self::Some(left), Self::Some(right)) = (left, right) else { + unreachable!("parent_hash called with non-concrete hash"); + }; + + let mut input = [0u8; 64]; + input[..32].copy_from_slice(left); + input[32..].copy_from_slice(right); + Self::Some(*blake3::hash(&input).as_bytes()) + } +} + +/// Hash a UTXO into the canonical leaf committed to by the accumulator. +pub fn utreexo_leaf_hash(outpoint: &OutPoint, output: &FilledOutput) -> Hash { + hashes::hash_with_scratch_buffer(&PointedOutput { + outpoint: *outpoint, + output: output.clone(), + }) +} + +/// Manage accumulator diffs. +/// Insertions and removals 'cancel out' exactly once. +/// Inserting twice will cause one insertion. +/// Removing twice will cause one deletion. +/// Inserting and then removing will have no overall effect, +/// but a second removal will still cause a deletion. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AccumulatorDiff { + /// `true` indicates insertion, `false` indicates removal. + diff: LinkedHashMap<[u8; 32], bool>, + /// Total number of insertions still represented in `diff`. + insertions: usize, + /// Total number of deletions still represented in `diff`. + deletions: usize, +} + +impl AccumulatorDiff { + pub fn with_capacity(capacity: usize) -> Self { + Self { + diff: LinkedHashMap::with_capacity(capacity), + insertions: 0, + deletions: 0, + } + } + + pub fn insert(&mut self, utxo_hash: [u8; 32]) { + match self.diff.entry(utxo_hash) { + Entry::Occupied(entry) => { + if !entry.get() { + entry.remove(); + debug_assert!(self.deletions > 0); + self.deletions -= 1; + } + } + Entry::Vacant(entry) => { + entry.insert(true); + self.insertions += 1; + } + } + } + + pub fn remove(&mut self, utxo_hash: [u8; 32]) { + match self.diff.entry(utxo_hash) { + Entry::Occupied(entry) => { + if *entry.get() { + entry.remove(); + debug_assert!(self.insertions > 0); + self.insertions -= 1; + } + } + Entry::Vacant(entry) => { + entry.insert(false); + self.deletions += 1; + } + } + } + + pub fn merge(&mut self, other: Self) { + for (utxo_hash, insert) in other.diff { + if insert { + self.insert(utxo_hash); + } else { + self.remove(utxo_hash); + } + } + } + + pub fn counts(&self) -> (usize, usize) { + (self.insertions, self.deletions) + } + + pub fn is_empty(&self) -> bool { + self.diff.is_empty() + } +} + +impl Serialize for AccumulatorDiff { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut insertions = Vec::with_capacity(self.insertions); + let mut deletions = Vec::with_capacity(self.deletions); + + for (hash, insert) in &self.diff { + if *insert { + insertions.push(*hash); + } else { + deletions.push(*hash); + } + } + + (insertions, deletions).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for AccumulatorDiff { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let (insertions, deletions): (Vec<[u8; 32]>, Vec<[u8; 32]>) = + Deserialize::deserialize(deserializer)?; + + let mut diff = + AccumulatorDiff::with_capacity(insertions.len() + deletions.len()); + + for hash in insertions { + diff.insert(hash); + } + + for hash in deletions { + diff.remove(hash); + } + + Ok(diff) + } +} + +#[repr(transparent)] +#[derive(Debug, Error)] +#[error("utreexo error: {0}")] +pub struct UtreexoError(String); + +impl UtreexoError { + pub(crate) fn custom(message: impl Into) -> Self { + Self(message.into()) + } +} + +#[derive(Default)] +#[repr(transparent)] +pub struct Accumulator(pub MemForest); + +unsafe impl Send for Accumulator {} +unsafe impl Sync for Accumulator {} + +impl Accumulator { + pub fn serialize_into( + &self, + writer: &mut W, + ) -> Result<(), UtreexoError> { + self.0 + .serialize(writer) + .map_err(|err| UtreexoError(err.to_string())) + } + + pub fn deserialize_from(bytes: &[u8]) -> Result { + let mem_forest = MemForest::deserialize(bytes) + .map_err(|err| UtreexoError(err.to_string()))?; + Ok(Self(mem_forest)) + } + + pub fn apply_diff( + &mut self, + diff: AccumulatorDiff, + ) -> Result<(), UtreexoError> { + let mut insertions = Vec::with_capacity(diff.insertions); + let mut deletions = Vec::with_capacity(diff.deletions); + + for (utxo_hash, insert) in diff.diff { + let utxo_hash = Blake3UtxoHash::Some(utxo_hash); + + if insert { + insertions.push(utxo_hash); + } else { + deletions.push(utxo_hash); + } + } + + for insertion in &insertions { + if self.0.prove(&[*insertion]).is_ok() { + return Err(UtreexoError(format!( + "attempted to insert an existing accumulator leaf: {insertion}" + ))); + } + } + + self.0 + .modify(&insertions, &deletions) + .map_err(UtreexoError)?; + + Ok(()) + } + + pub fn get_roots(&self) -> Vec { + self.0 + .get_roots() + .iter() + .map(|node| node.get_data()) + .collect() + } + + pub fn prove( + &self, + targets: &[Blake3UtxoHash], + ) -> Result, UtreexoError> { + self.0.prove(targets).map_err(UtreexoError) + } + + pub fn verify( + &self, + proof: &Proof, + del_hashes: &[Blake3UtxoHash], + ) -> Result { + self.0.verify(proof, del_hashes).map_err(UtreexoError) + } +} + +impl<'de> Deserialize<'de> for Accumulator { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes: Vec = + as Deserialize>::deserialize(deserializer)?; + Self::deserialize_from(&bytes) + .map_err(::custom) + } +} + +impl Serialize for Accumulator { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut bytes = Vec::new(); + self.serialize_into(&mut bytes) + .map_err(::custom)?; + as Serialize>::serialize(&bytes, serializer) + } +} + +#[cfg(test)] +mod accumulator_tests { + use rustreexo::accumulator::node_hash::AccumulatorHash as _; + + use super::{ + Accumulator, AccumulatorDiff, Address, Blake3UtxoHash, FilledOutput, + FilledOutputContent, OutPoint, utreexo_leaf_hash, + }; + + #[test] + fn accumulator_hash_tags_roundtrip_distinct_sentinels() { + for expected in [ + Blake3UtxoHash::Empty, + Blake3UtxoHash::Placeholder, + Blake3UtxoHash::Some([0; 32]), + Blake3UtxoHash::Some([42; 32]), + ] { + let mut bytes = Vec::new(); + expected.write(&mut bytes).unwrap(); + let actual = Blake3UtxoHash::read(&mut &*bytes).unwrap(); + assert_eq!(actual, expected); + } + } + + #[test] + fn accumulator_with_deleted_root_roundtrips() { + let mut accumulator = Accumulator::default(); + let mut additions = AccumulatorDiff::default(); + additions.insert([1; 32]); + additions.insert([2; 32]); + accumulator.apply_diff(additions).unwrap(); + + let mut deletions = AccumulatorDiff::default(); + deletions.remove([1; 32]); + deletions.remove([2; 32]); + accumulator.apply_diff(deletions).unwrap(); + assert_eq!(accumulator.get_roots(), vec![Blake3UtxoHash::Empty]); + + let encoded = bincode::serialize(&accumulator).unwrap(); + let decoded: Accumulator = bincode::deserialize(&encoded).unwrap(); + assert_eq!(decoded.get_roots(), accumulator.get_roots()); + assert_eq!(decoded.0.leaves, accumulator.0.leaves); + assert_eq!(bincode::serialize(&decoded).unwrap(), encoded); + } + + #[test] + fn accumulator_rejects_duplicate_leaf_insertions() { + let mut accumulator = Accumulator::default(); + let mut insertion = AccumulatorDiff::default(); + insertion.insert([7; 32]); + accumulator.apply_diff(insertion.clone()).unwrap(); + let before = bincode::serialize(&accumulator).unwrap(); + + assert!(accumulator.apply_diff(insertion).is_err()); + assert_eq!(bincode::serialize(&accumulator).unwrap(), before); + } + + #[test] + fn utreexo_leaf_hash_golden_vector() { + let outpoint = OutPoint::Regular { + txid: [1; 32].into(), + vout: 7, + }; + let output = FilledOutput { + address: Address::ALL_ZEROS, + content: FilledOutputContent::new_bitcoin_value( + bitcoin::Amount::from_sat(42), + ), + memo: b"utreexo".to_vec(), + }; + + assert_eq!( + hex::encode(utreexo_leaf_hash(&outpoint, &output)), + "3d6f84894a0d7586c1fdc5c7f42f3268aedef0f14a97633241bab06c66ccbb19" + ); + } + + #[test] + fn merging_diffs_preserves_net_utxo_changes() { + let mut diff = AccumulatorDiff::default(); + diff.insert([1; 32]); + diff.remove([2; 32]); + + let mut next = AccumulatorDiff::default(); + next.remove([1; 32]); + next.insert([2; 32]); + next.insert([3; 32]); + diff.merge(next); + + let mut expected = AccumulatorDiff::default(); + expected.insert([3; 32]); + assert_eq!(diff, expected); + } +} + /// Transaction index #[derive(Clone, Copy, Debug, Deserialize, Serialize, ToSchema)] pub struct TxIn { @@ -606,7 +1220,7 @@ pub enum BmmResult { /// A tip refers to both a sidechain block AND the mainchain block that commits /// to it. #[derive( - BorshSerialize, + borsh::BorshSerialize, Clone, Copy, Debug, @@ -637,7 +1251,7 @@ pub enum Network { /// Semver-compatible version #[derive( - BorshSerialize, + borsh::BorshSerialize, Clone, Copy, Debug, @@ -683,10 +1297,59 @@ pub(crate) static VERSION: LazyLock = LazyLock::new(|| { semver::Version::parse(VERSION_STR).unwrap().into() }); +#[cfg(test)] +mod merkle_tests { + use super::*; + + fn tx(memo: &[u8]) -> Transaction { + Transaction { + memo: memo.to_vec(), + ..Default::default() + } + } + + #[test] + fn tx_merkle_proof_verifies_membership() { + let coinbase = Vec::new(); + let txs = vec![tx(b"tx-0"), tx(b"tx-1"), tx(b"tx-2"), tx(b"tx-3")]; + + let root = Body::compute_merkle_root(&coinbase, &txs); + let proof = Body::compute_tx_merkle_proof(&coinbase, &txs, 2); + let leaf = hashes::hash_with_scratch_buffer(&txs[2]); + + assert!(proof.verify(leaf, root)); + } + + #[test] + fn tx_merkle_proof_rejects_non_member() { + let coinbase = Vec::new(); + let txs = vec![tx(b"tx-0"), tx(b"tx-1"), tx(b"tx-2"), tx(b"tx-3")]; + let other = tx(b"other"); + + let root = Body::compute_merkle_root(&coinbase, &txs); + let proof = Body::compute_tx_merkle_proof(&coinbase, &txs, 2); + let leaf = hashes::hash_with_scratch_buffer(&other); + + assert!(!proof.verify(leaf, root)); + } + + #[test] + fn tx_merkle_proof_rejects_wrong_position() { + let coinbase = Vec::new(); + let txs = vec![tx(b"tx-0"), tx(b"tx-1"), tx(b"tx-2"), tx(b"tx-3")]; + + let root = Body::compute_merkle_root(&coinbase, &txs); + let proof = Body::compute_tx_merkle_proof(&coinbase, &txs, 2); + let leaf = hashes::hash_with_scratch_buffer(&txs[1]); + + assert!(!proof.verify(leaf, root)); + } +} + #[cfg(test)] mod withdrawal_bundle_order_regression { use super::*; - use std::collections::{BTreeMap, HashMap}; + use std::collections::BTreeMap; use bitcoin::{Address, Amount, address::NetworkUnchecked}; @@ -697,15 +1360,15 @@ mod withdrawal_bundle_order_regression { .parse() .unwrap(); AggregatedWithdrawal { - spend_utxos: HashMap::new(), + spend_utxos: BTreeMap::new(), main_address: addr, value: Amount::from_sat(value), main_fee: Amount::from_sat(main_fee), } } - // Build the bundle m6id exactly as `collect_withdrawal_bundle` does, for a given - // (HashMap-determined) input order. + // Build the bundle m6id exactly as `collect_withdrawal_bundle` does, for a + // given input order. fn bundle_m6id(mut aggregated: Vec) -> M6id { aggregated.sort_by_key(|a| std::cmp::Reverse(a.clone())); let outputs: Vec = aggregated diff --git a/lib/types/transaction/mod.rs b/lib/types/transaction/mod.rs index 87b32647..9e776d2f 100644 --- a/lib/types/transaction/mod.rs +++ b/lib/types/transaction/mod.rs @@ -1,5 +1,6 @@ use std::{ borrow::Borrow, + borrow::Cow, cmp::Ordering, collections::{HashMap, HashSet}, io::Cursor, @@ -17,7 +18,7 @@ use crate::{ types::{ AmountOverflowError, BitAssetData, BitAssetDataUpdates, GetAddress, GetBitcoinValue, - address::Address, + address::{ADDRESS_SIZE, Address}, hashes::{ self, AssetId, BitAssetId, DutchAuctionId, Hash, M6id, MerkleRoot, Txid, @@ -115,6 +116,7 @@ impl std::fmt::Display for OutPoint { } const OUTPOINT_KEY_SIZE: usize = 37; +const ADDRESS_OUTPOINT_KEY_SIZE: usize = ADDRESS_SIZE + OUTPOINT_KEY_SIZE; /// Fixed-width key for OutPoint based on its canonical Borsh encoding. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -203,8 +205,8 @@ impl<'a> BytesEncode<'a> for OutPointKey { #[inline] fn bytes_encode( item: &'a Self::EItem, - ) -> Result, BoxedError> { - Ok(std::borrow::Cow::Borrowed(item.as_ref())) + ) -> Result, BoxedError> { + Ok(Cow::Borrowed(item.as_ref())) } } @@ -225,6 +227,164 @@ impl<'a> BytesDecode<'a> for OutPointKey { } } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct AddressOutPointKey([u8; ADDRESS_OUTPOINT_KEY_SIZE]); + +impl AddressOutPointKey { + #[inline] + pub fn new(address: Address, outpoint: OutPointKey) -> Self { + let mut bytes = [0u8; ADDRESS_OUTPOINT_KEY_SIZE]; + bytes[..ADDRESS_SIZE].copy_from_slice(&address.0); + bytes[ADDRESS_SIZE..].copy_from_slice(outpoint.as_bytes()); + Self(bytes) + } + + #[inline] + pub fn start(address: Address) -> Self { + let mut bytes = [0u8; ADDRESS_OUTPOINT_KEY_SIZE]; + bytes[..ADDRESS_SIZE].copy_from_slice(&address.0); + Self(bytes) + } + + #[inline] + pub fn end(address: Address) -> Self { + let mut bytes = [0xffu8; ADDRESS_OUTPOINT_KEY_SIZE]; + bytes[..ADDRESS_SIZE].copy_from_slice(&address.0); + Self(bytes) + } + + #[inline] + pub fn address(&self) -> Address { + let mut address = [0u8; ADDRESS_SIZE]; + address.copy_from_slice(&self.0[..ADDRESS_SIZE]); + Address(address) + } + + #[inline] + pub fn outpoint_key(&self) -> OutPointKey { + ::bytes_decode(&self.0[ADDRESS_SIZE..]) + .expect("AddressOutPointKey should contain a valid OutPointKey") + } +} + +impl Ord for AddressOutPointKey { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl PartialOrd for AddressOutPointKey { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl AsRef<[u8]> for AddressOutPointKey { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'a> BytesEncode<'a> for AddressOutPointKey { + type EItem = AddressOutPointKey; + + #[inline] + fn bytes_encode( + item: &'a Self::EItem, + ) -> Result, BoxedError> { + Ok(Cow::Borrowed(item.as_ref())) + } +} + +impl<'a> BytesDecode<'a> for AddressOutPointKey { + type DItem = AddressOutPointKey; + + #[inline] + fn bytes_decode(bytes: &'a [u8]) -> Result { + ::bytes_decode(&bytes[ADDRESS_SIZE..])?; + let mut key = [0u8; ADDRESS_OUTPOINT_KEY_SIZE]; + key.copy_from_slice(bytes); + Ok(Self(key)) + } +} + +const TXID_SIZE: usize = blake3::OUT_LEN; +const ADDR_TXID_DB_KEY_SIZE: usize = ADDRESS_SIZE + TXID_SIZE; + +/// Fixed-width txdb key: address || txid. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct AddressTxidKey([u8; ADDR_TXID_DB_KEY_SIZE]); + +impl AddressTxidKey { + #[inline] + pub fn new(address: Address, txid: Txid) -> Self { + let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE]; + key[..ADDRESS_SIZE].copy_from_slice(&address.0); + key[ADDRESS_SIZE..].copy_from_slice(txid.as_slice()); + Self(key) + } + + #[inline] + pub fn start(address: Address) -> Self { + let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE]; + key[..ADDRESS_SIZE].copy_from_slice(&address.0); + Self(key) + } + + #[inline] + pub fn end(address: Address) -> Self { + let mut key = [0xffu8; ADDR_TXID_DB_KEY_SIZE]; + key[..ADDRESS_SIZE].copy_from_slice(&address.0); + Self(key) + } + + #[inline] + pub fn address(&self) -> Address { + let mut address = [0u8; ADDRESS_SIZE]; + address.copy_from_slice(&self.0[..ADDRESS_SIZE]); + Address(address) + } + + #[inline] + pub fn txid(&self) -> Txid { + let mut txid = [0u8; TXID_SIZE]; + txid.copy_from_slice(&self.0[ADDRESS_SIZE..]); + Txid(txid) + } +} + +impl AsRef<[u8]> for AddressTxidKey { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'a> BytesEncode<'a> for AddressTxidKey { + type EItem = AddressTxidKey; + + #[inline] + fn bytes_encode( + item: &'a Self::EItem, + ) -> Result, BoxedError> { + Ok(Cow::Borrowed(item.as_ref())) + } +} + +impl<'a> BytesDecode<'a> for AddressTxidKey { + type DItem = AddressTxidKey; + + #[inline] + fn bytes_decode(bytes: &'a [u8]) -> Result { + let mut key = [0u8; ADDR_TXID_DB_KEY_SIZE]; + key.copy_from_slice(bytes); + Ok(Self(key)) + } +} + #[cfg(test)] mod test { use bitcoin::hashes::Hash as _; diff --git a/lib/types/transaction/output.rs b/lib/types/transaction/output.rs index 5ee05c2f..f7c8ffc7 100644 --- a/lib/types/transaction/output.rs +++ b/lib/types/transaction/output.rs @@ -605,6 +605,7 @@ mod filled_content { FilledContent!( pub FilledContent, attrs: [derive( + borsh::BorshSerialize, Clone, Debug, Eq,