From 222244992a4a22c420cc2610dcf1fc389ba547be Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 23 Jul 2026 20:43:07 +0800 Subject: [PATCH 01/18] update deps/deny.toml --- Cargo.lock | 1216 ++++++----------------------- Cargo.toml | 2 +- app/Cargo.toml | 5 +- app/gui/block_explorer.rs | 2 +- app/gui/coins/mod.rs | 4 +- app/gui/coins/transfer_receive.rs | 4 +- app/gui/console_logs.rs | 2 +- app/gui/mod.rs | 47 +- app/gui/parent_chain/mod.rs | 4 +- app/gui/parent_chain/transfer.rs | 4 +- deny.toml | 5 +- 11 files changed, 283 insertions(+), 1012 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 610c6685..3f890fd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - [[package]] name = "accesskit" version = "0.24.1" @@ -27,110 +11,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "accesskit_atspi_common" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" -dependencies = [ - "accesskit", - "accesskit_consumer 0.36.0", - "atspi-common", - "phf 0.13.1", - "serde", - "zvariant", -] - -[[package]] -name = "accesskit_consumer" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - -[[package]] -name = "accesskit_consumer" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - -[[package]] -name = "accesskit_consumer" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950720ce064757a1b629caad3a408e8d2c63bb01f29b8a3ff8daa331053ffeb" -dependencies = [ - "accesskit", - "hashbrown 0.16.1", -] - -[[package]] -name = "accesskit_macos" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cb8b66cef272d48161b02a6317cc2bdd5f98bb0a5e79c68f704a5862aa396b" -dependencies = [ - "accesskit", - "accesskit_consumer 0.37.0", - "hashbrown 0.16.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "accesskit_unix" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" -dependencies = [ - "accesskit", - "accesskit_atspi_common", - "async-channel", - "async-executor", - "async-task", - "atspi", - "futures-lite", - "futures-util", - "serde", - "zbus", -] - -[[package]] -name = "accesskit_windows" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" -dependencies = [ - "accesskit", - "accesskit_consumer 0.35.0", - "hashbrown 0.16.1", - "static_assertions", - "windows", - "windows-core", -] - -[[package]] -name = "accesskit_winit" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_unix", - "accesskit_windows", - "raw-window-handle", - "winit", -] - [[package]] name = "adler2" version = "2.0.1" @@ -200,12 +80,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android-activity" version = "0.6.0" @@ -213,11 +87,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.10.0", + "bitflags 2.13.1", "cc", "cesu8", - "jni", - "jni-sys", + "jni 0.21.1", + "jni-sys 0.3.0", "libc", "log", "ndk", @@ -354,15 +228,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -375,18 +240,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compression" version = "0.4.42" @@ -399,20 +252,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - [[package]] name = "async-io" version = "2.6.0" @@ -442,59 +281,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 1.1.3", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.3", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -525,43 +311,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atspi" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" -dependencies = [ - "atspi-common", - "atspi-proxies", -] - -[[package]] -name = "atspi-common" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" -dependencies = [ - "enumflags2", - "serde", - "static_assertions", - "zbus", - "zbus-lockstep", - "zbus-lockstep-macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "atspi-proxies" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" -dependencies = [ - "atspi-common", - "serde", - "zbus", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -975,9 +724,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -1038,19 +787,6 @@ dependencies = [ "objc2 0.6.4", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "borsh" version = "1.6.0" @@ -1124,9 +860,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] @@ -1169,7 +905,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.1", "log", "polling", "rustix 0.38.44", @@ -1183,7 +919,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "polling", "rustix 1.1.3", "slab", @@ -1361,16 +1097,14 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "serde", - "termcolor", - "unicode-width 0.2.2", + "unicode-width 0.1.14", ] [[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", ] @@ -1848,7 +1582,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "objc2 0.6.4", ] @@ -1899,7 +1633,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" dependencies = [ - "phf 0.11.3", + "phf", ] [[package]] @@ -1922,9 +1656,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecolor" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e" +checksum = "6758be723a3f298bbfda4db75748bc2ba0abafe096b6383c7c32da264764fbc3" dependencies = [ "bytemuck", "emath", @@ -1992,9 +1726,9 @@ dependencies = [ [[package]] name = "eframe" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98fe83b2589105b69dd25ca1e0fa2135a6e864d502fd8e08978f937e128cfef" +checksum = "8dc8234e2f681b2afd2b2e8c332fa614de2fddbdcb28d0acf5c420449682ea90" dependencies = [ "ahash", "bytemuck", @@ -2003,6 +1737,7 @@ dependencies = [ "egui-wgpu", "egui-winit", "egui_glow", + "glow", "glutin", "glutin-winit", "image", @@ -2013,7 +1748,6 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "pollster", "profiling", "raw-window-handle", "static_assertions", @@ -2021,22 +1755,22 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "web-time", - "wgpu", "windows-sys 0.61.2", "winit", ] [[package]] name = "egui" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" +checksum = "2796c98d50b79631281d516343a6f6e93c0666462ca36e2c93b39f25d7793325" dependencies = [ "accesskit", "ahash", - "bitflags 2.10.0", + "bitflags 2.13.1", "emath", "epaint", + "itertools 0.14.0", "log", "nohash-hasher", "profiling", @@ -2046,9 +1780,9 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f0c0559ac5598a1b887a6206dccbab7e3e6246c57cb00ae287262bd44776c9c" +checksum = "d2e6cfac0725563555fa4f91e9f799b9d7c6c5dd831fca6abc8234afc64b7a34" dependencies = [ "ahash", "bytemuck", @@ -2066,11 +1800,10 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "967c5b323625d46d46a59b5daba3fef742248d27693cc18972458619858c4239" +checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db" dependencies = [ - "accesskit_winit", "arboard", "bytemuck", "egui", @@ -2088,9 +1821,9 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b652957fa7e1ab01e8fecbfbf4e35f6e43a53fa98af8a562b50d5403cd44b9" +checksum = "52274b9bfb8d8e252cd0f00c9f6214f360d75a0962baa77a2d62ddb002fe99d4" dependencies = [ "bytemuck", "egui", @@ -2098,8 +1831,6 @@ dependencies = [ "log", "memoffset", "profiling", - "wasm-bindgen", - "web-sys", "winit", ] @@ -2129,9 +1860,9 @@ dependencies = [ [[package]] name = "emath" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53f0d33a479321da6b0caa71366c9f67e8a2c149762d90bdc0d16e601ee8ecb" +checksum = "cd4ec073c9898516584d8c6cfdcee95b530b3d941cd5031ef4050aa36812308b" dependencies = [ "bytemuck", ] @@ -2142,12 +1873,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -2168,39 +1893,18 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "epaint" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" +checksum = "4e60a8888b51da911df23918fd7301359b1d43a406a0ff3b8863af093dd7fc6c" dependencies = [ "ahash", "bytemuck", "ecolor", "emath", - "epaint_default_fonts", "font-types", + "harfrust", "log", "nohash-hasher", "parking_lot", @@ -2208,15 +1912,11 @@ dependencies = [ "self_cell", "skrifa", "smallvec", + "unicode-general-category", + "unicode-segmentation", "vello_cpu", ] -[[package]] -name = "epaint_default_fonts" -version = "0.34.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da" - [[package]] name = "equivalent" version = "1.0.2" @@ -2439,12 +2139,9 @@ dependencies = [ [[package]] name = "fearless_simd" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" -dependencies = [ - "bytemuck", -] +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" [[package]] name = "fiat-crypto" @@ -2642,10 +2339,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "parking", "pin-project-lite", ] @@ -2781,6 +2475,22 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "log", + "peniko", + "skrifa", + "smallvec", + "vello_common", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -2845,7 +2555,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.1", "cfg_aliases", "cgl", "dispatch2", @@ -2927,37 +2637,12 @@ dependencies = [ ] [[package]] -name = "gpu-allocator" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" -dependencies = [ - "ash", - "hashbrown 0.16.1", - "log", - "presser", - "thiserror 2.0.18", - "windows", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.10.0", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" +name = "guillotiere" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" dependencies = [ - "bitflags 2.10.0", + "euclid", ] [[package]] @@ -2991,6 +2676,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "harfrust" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "read-fonts", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -3023,8 +2720,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.2.0", ] @@ -3094,7 +2789,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a56c94661ddfb51aa9cdfbf102cfcc340aa69267f95ebccc4af08d7c530d393" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "byteorder", "heed-traits", "heed-types", @@ -3635,19 +3330,68 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.0", "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.117", +] + [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -4021,17 +3765,6 @@ dependencies = [ "cpufeatures 0.2.17", ] -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - [[package]] name = "khronos_api" version = "3.1.0" @@ -4146,7 +3879,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "libc", "redox_syscall 0.7.0", ] @@ -4235,9 +3968,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" @@ -4480,13 +4213,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.1", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -4499,7 +4232,6 @@ dependencies = [ "num-traits", "once_cell", "rustc-hash 1.1.0", - "spirv", "thiserror 2.0.18", "unicode-ident", ] @@ -4510,8 +4242,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.1", + "jni-sys 0.3.0", "log", "ndk-sys", "num_enum", @@ -4531,7 +4263,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.0", ] [[package]] @@ -4681,14 +4413,14 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "objc2-quartz-core", ] [[package]] @@ -4697,7 +4429,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.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -4710,7 +4442,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.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4734,7 +4466,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.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4746,7 +4478,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.1", "dispatch2", "objc2 0.6.4", ] @@ -4757,7 +4489,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.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -4773,7 +4505,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", + "objc2-metal", ] [[package]] @@ -4800,7 +4532,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.1", "block2 0.5.1", "dispatch", "libc", @@ -4813,7 +4545,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.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -4825,7 +4557,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.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4848,48 +4580,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-metal" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" -dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "objc2-metal 0.3.2", + "objc2-metal", ] [[package]] @@ -4908,7 +4615,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.1", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -4917,7 +4624,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core 0.2.2", + "objc2-quartz-core", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -4929,7 +4636,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.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4952,7 +4659,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.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -4983,7 +4690,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -5036,25 +4743,6 @@ dependencies = [ "libredox", ] -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "ordermap" version = "1.2.0" @@ -5089,15 +4777,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - [[package]] name = "page_size" version = "0.6.0" @@ -5176,9 +4855,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", @@ -5210,19 +4889,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", - "serde", + "phf_macros", + "phf_shared", ] [[package]] @@ -5231,41 +4899,18 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.3", + "phf_shared", "rand 0.8.6", ] -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.117", @@ -5280,15 +4925,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -5321,17 +4957,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -5354,7 +4979,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -5386,12 +5011,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "pollster" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" - [[package]] name = "polonius-the-crab" version = "0.2.1" @@ -5468,12 +5087,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - [[package]] name = "prettyplease" version = "0.2.37" @@ -5641,7 +5254,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "memchr", "unicase", ] @@ -5687,9 +5300,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.39.4" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", ] @@ -5857,38 +5470,20 @@ dependencies = [ "rand_core 0.9.5", ] -[[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - [[package]] name = "raw-cpuid" version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", ] [[package]] name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "raw-window-metal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" -dependencies = [ - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", -] +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" @@ -5925,9 +5520,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.37.0" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" dependencies = [ "bytemuck", "font-types", @@ -5948,7 +5543,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", ] [[package]] @@ -5957,7 +5552,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", ] [[package]] @@ -6130,7 +5725,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.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink 0.9.1", @@ -6175,7 +5770,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", @@ -6188,7 +5783,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "errno 0.3.14", "libc", "linux-raw-sys 0.11.0", @@ -6241,7 +5836,7 @@ checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -6262,7 +5857,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -6408,19 +6003,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit 0.19.2", - "tiny-skia", -] - [[package]] name = "sdd" version = "4.8.6" @@ -6457,7 +6039,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6550,17 +6132,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6666,6 +6237,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.2" @@ -6674,9 +6261,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "skrifa" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" dependencies = [ "bytemuck", "read-fonts", @@ -6709,7 +6296,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.1", "calloop 0.13.0", "calloop-wayland-source 0.3.0", "cursor-icon", @@ -6734,7 +6321,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "calloop 0.14.3", "calloop-wayland-source 0.4.1", "cursor-icon", @@ -6843,15 +6430,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "spirv" -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", -] - [[package]] name = "spki" version = "0.7.3" @@ -6874,12 +6452,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - [[package]] name = "strsim" version = "0.11.1" @@ -7003,15 +6575,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -7280,31 +6843,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - [[package]] name = "tinystr" version = "0.8.2" @@ -7542,7 +7080,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -7684,12 +7222,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - [[package]] name = "type-map" version = "0.5.1" @@ -7720,17 +7252,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - [[package]] name = "ulid" version = "1.2.1" @@ -7747,6 +7268,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -7764,9 +7291,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" @@ -7863,7 +7390,6 @@ checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", - "serde_core", "wasm-bindgen", ] @@ -7881,27 +7407,29 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vello_common" -version = "0.0.6" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" dependencies = [ "bytemuck", "fearless_simd", - "hashbrown 0.16.1", + "guillotiere", + "hashbrown 0.17.1", "log", "peniko", - "skrifa", "smallvec", + "thiserror 2.0.18", ] [[package]] name = "vello_cpu" -version = "0.0.6" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23" dependencies = [ "bytemuck", - "hashbrown 0.16.1", + "glifo", + "hashbrown 0.17.1", "vello_common", ] @@ -8062,7 +7590,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -8088,7 +7616,7 @@ version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "rustix 1.1.3", "wayland-backend", "wayland-scanner", @@ -8100,7 +7628,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "cursor-icon", "wayland-backend", ] @@ -8122,7 +7650,7 @@ version = "0.32.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8134,7 +7662,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.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8147,7 +7675,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791c58fdeec5406aa37169dd815327d1e47f334219b523444bc26d70ceb4c34e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8160,7 +7688,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa98634619300a535a9a97f338aed9a5ff1e01a461943e8346ff4ae26007306b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8173,7 +7701,7 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8182,9 +7710,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", "quick-xml", @@ -8225,12 +7753,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", @@ -8280,12 +7808,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.1", "bytemuck", "cfg-if", "cfg_aliases", @@ -8293,8 +7821,6 @@ dependencies = [ "hashbrown 0.16.1", "js-sys", "log", - "naga", - "parking_lot", "portable-atomic", "profiling", "raw-window-handle", @@ -8310,14 +7836,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.1", "bytemuck", "cfg_aliases", "document-features", @@ -8333,110 +7859,47 @@ dependencies = [ "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.18", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", "wgpu-types", ] -[[package]] -name = "wgpu-core-deps-apple" -version = "29.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e51b5447e144b3dbba4feb01f80f4fa21696fa0cd99afb2c3df1affd6fdb28" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "29.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3487cd6293a963bc5c0c0396f6a2192043c50003c07f4efdccbad3d90ec9d819" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-wasm" -version = "29.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2f2fb042f36920771deb0b966543c5751b18f3d327760ffc90f74e20b2dcd4" -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", - "block2 0.6.2", - "bytemuck", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", - "glow", - "glutin_wgl_sys", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", "libloading", "log", "naga", - "ndk-sys", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "objc2-metal 0.3.2", - "objc2-quartz-core 0.3.2", - "once_cell", - "ordered-float", - "parking_lot", "portable-atomic", "portable-atomic-util", - "profiling", - "range-alloc", "raw-window-handle", - "raw-window-metal", "renderdoc-sys", - "smallvec", "thiserror 2.0.18", - "wasm-bindgen", - "wayland-sys", - "web-sys", "wgpu-naga-bridge", "wgpu-types", - "windows", - "windows-core", - "windows-result", ] [[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", @@ -8444,11 +7907,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.1", "bytemuck", "js-sys", "log", @@ -8508,27 +7971,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -8542,17 +7984,6 @@ dependencies = [ "windows-strings", ] -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -8581,16 +8012,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -8691,15 +8112,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8841,7 +8253,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.10.0", + "bitflags 2.13.1", "block2 0.5.1", "bytemuck", "calloop 0.13.0", @@ -8865,7 +8277,6 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", - "sctk-adwaita", "smithay-client-toolkit 0.19.2", "smol_str 0.2.2", "tracing", @@ -8951,7 +8362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.13.1", "indexmap 2.14.0", "log", "serde", @@ -9031,7 +8442,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.1", "dlib", "log", "once_cell", @@ -9082,103 +8493,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.3", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus-lockstep" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" -dependencies = [ - "zbus_xml", - "zvariant", -] - -[[package]] -name = "zbus-lockstep-macros" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus-lockstep", - "zbus_xml", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" -dependencies = [ - "serde", - "winnow", - "zvariant", -] - -[[package]] -name = "zbus_xml" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ab0513c0a66a60a8718d5ad712a5094845a20d95b5c1c81e2bd8e904a52b4f" -dependencies = [ - "serde", - "winnow", - "zbus_names", - "zvariant", -] - [[package]] name = "zerocopy" version = "0.8.35" @@ -9352,43 +8666,3 @@ checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] - -[[package]] -name = "zvariant" -version = "5.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "winnow", -] diff --git a/Cargo.toml b/Cargo.toml index 3c8aa0ee..5ba8b121 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ dotenvy = "0.15.7" ed25519-dalek = "2.1.1" ed25519-dalek-bip32 = "0.3.0" educe = { version = "0.6.0", default-features = false } -eframe = "0.34.0" +eframe = { version = "0.35.0", default-features = false } error-fatality = "0.1.2" fallible-iterator = "0.3.0" futures = { version = "0.3.30", default-features = false } diff --git a/app/Cargo.toml b/app/Cargo.toml index 7f0f7ca9..52c83c62 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -12,7 +12,6 @@ bincode = { workspace = true } bitcoin = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } dirs = { workspace = true } -eframe = { workspace = true } fallible-iterator = { workspace = true } futures = { workspace = true } http = { workspace = true } @@ -44,6 +43,10 @@ url = { workspace = true } utoipa = { workspace = true } uuid = { workspace = true } +[dependencies.eframe] +workspace = true +features = ["glow", "wayland", "web_screen_reader", "x11"] + [dependencies.tokio] workspace = true features = ["macros", "rt-multi-thread", "signal"] diff --git a/app/gui/block_explorer.rs b/app/gui/block_explorer.rs index 554ac079..114e7cf1 100644 --- a/app/gui/block_explorer.rs +++ b/app/gui/block_explorer.rs @@ -35,7 +35,7 @@ impl BlockExplorer { None } }; - egui::CentralPanel::default().show_inside(ui, |ui| { + egui::CentralPanel::default().show(ui, |ui| { ui.heading("Block"); ui.horizontal(|ui| { if ui.button("<").clicked() && self.height > 0 { diff --git a/app/gui/coins/mod.rs b/app/gui/coins/mod.rs index 61f86bc8..eb309acb 100644 --- a/app/gui/coins/mod.rs +++ b/app/gui/coins/mod.rs @@ -37,7 +37,7 @@ impl Coins { } pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - egui::Panel::top("coins_tabs").show_inside(ui, |ui| { + egui::Panel::top("coins_tabs").show(ui, |ui| { ui.horizontal(|ui| { Tab::iter().for_each(|tab_variant| { let tab_name = tab_variant.to_string(); @@ -45,7 +45,7 @@ impl Coins { }) }); }); - egui::CentralPanel::default().show_inside(ui, |ui| match self.tab { + egui::CentralPanel::default().show(ui, |ui| match self.tab { Tab::TransferReceive => { let () = self.transfer_receive.show(app, ui); } diff --git a/app/gui/coins/transfer_receive.rs b/app/gui/coins/transfer_receive.rs index 55ea3823..d2a6a036 100644 --- a/app/gui/coins/transfer_receive.rs +++ b/app/gui/coins/transfer_receive.rs @@ -151,13 +151,13 @@ impl TransferReceive { egui::Panel::left("transfer") .exact_size(ui.available_width() / 2.) .resizable(false) - .show_inside(ui, |ui| { + .show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Transfer"); self.transfer.show(app, ui); }) }); - egui::CentralPanel::default().show_inside(ui, |ui| { + egui::CentralPanel::default().show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Receive"); self.receive.show(app, ui); diff --git a/app/gui/console_logs.rs b/app/gui/console_logs.rs index 2541c094..ffc72b55 100644 --- a/app/gui/console_logs.rs +++ b/app/gui/console_logs.rs @@ -89,7 +89,7 @@ impl ConsoleLogs { } pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - egui::Panel::bottom("command_input").show_inside(ui, |ui| { + egui::Panel::bottom("command_input").show(ui, |ui| { let command_input = TextEdit::multiline(&mut self.command_input) .font(TextStyle::Monospace) .desired_width(f32::INFINITY) diff --git a/app/gui/mod.rs b/app/gui/mod.rs index 02db2a07..ef1f746d 100644 --- a/app/gui/mod.rs +++ b/app/gui/mod.rs @@ -226,13 +226,13 @@ impl eframe::App for EguiApp { if let Some(app) = self.app.as_ref() && !app.wallet.has_seed().unwrap_or(false) { - egui::CentralPanel::default().show_inside(ui, |ui| { + egui::CentralPanel::default().show(ui, |ui| { egui::Window::new("Set Seed").show(ui, |ui| { self.set_seed.show(app, ui); }); }); } else { - egui::Panel::top("tabs").show_inside(ui, |ui| { + egui::Panel::top("tabs").show(ui, |ui| { ui.horizontal(|ui| { Tab::iter().for_each(|tab_variant| { let tab_name = tab_variant.to_string(); @@ -244,29 +244,26 @@ impl eframe::App for EguiApp { }) }); }); - egui::Panel::bottom("bottom_panel").show_inside(ui, |ui| { - self.bottom_panel.show(&mut self.miner, ui) - }); - egui::CentralPanel::default().show_inside(ui, |ui| { - match self.tab { - Tab::ParentChain => { - self.parent_chain.show(self.app.as_ref(), ui) - } - Tab::Coins => { - self.coins.show(self.app.as_ref(), ui); - } - Tab::MemPoolExplorer => { - self.mempool_explorer.show(self.app.as_ref(), ui); - } - Tab::BlockExplorer => { - self.block_explorer.show(self.app.as_ref(), ui); - } - Tab::Withdrawals => { - self.withdrawals.show(self.app.as_ref(), ui); - } - Tab::ConsoleLogs => { - self.console_logs.show(self.app.as_ref(), ui); - } + egui::Panel::bottom("bottom_panel") + .show(ui, |ui| self.bottom_panel.show(&mut self.miner, ui)); + egui::CentralPanel::default().show(ui, |ui| match self.tab { + Tab::ParentChain => { + self.parent_chain.show(self.app.as_ref(), ui) + } + Tab::Coins => { + self.coins.show(self.app.as_ref(), ui); + } + Tab::MemPoolExplorer => { + self.mempool_explorer.show(self.app.as_ref(), ui); + } + Tab::BlockExplorer => { + self.block_explorer.show(self.app.as_ref(), ui); + } + Tab::Withdrawals => { + self.withdrawals.show(self.app.as_ref(), ui); + } + Tab::ConsoleLogs => { + self.console_logs.show(self.app.as_ref(), ui); } }); } diff --git a/app/gui/parent_chain/mod.rs b/app/gui/parent_chain/mod.rs index 275c4958..4ad301e6 100644 --- a/app/gui/parent_chain/mod.rs +++ b/app/gui/parent_chain/mod.rs @@ -35,7 +35,7 @@ impl ParentChain { } pub fn show(&mut self, app: Option<&App>, ui: &mut egui::Ui) { - egui::Panel::top("parent_chain_tabs").show_inside(ui, |ui| { + egui::Panel::top("parent_chain_tabs").show(ui, |ui| { ui.horizontal(|ui| { Tab::iter().for_each(|tab_variant| { let tab_name = tab_variant.to_string(); @@ -43,7 +43,7 @@ impl ParentChain { }) }); }); - egui::CentralPanel::default().show_inside(ui, |ui| match self.tab { + egui::CentralPanel::default().show(ui, |ui| match self.tab { Tab::Transfer => { self.transfer.show(app, ui); } diff --git a/app/gui/parent_chain/transfer.rs b/app/gui/parent_chain/transfer.rs index 7441717b..41247dee 100644 --- a/app/gui/parent_chain/transfer.rs +++ b/app/gui/parent_chain/transfer.rs @@ -292,13 +292,13 @@ impl Transfer { egui::Panel::left("deposit") .exact_size(ui.available_width() / 2.) .resizable(false) - .show_inside(ui, |ui| { + .show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Deposit"); self.deposit.show(app, ui); }) }); - egui::CentralPanel::default().show_inside(ui, |ui| { + egui::CentralPanel::default().show(ui, |ui| { ui.vertical_centered(|ui| { ui.heading("Withdrawal"); self.withdrawal.show(app, ui); diff --git a/deny.toml b/deny.toml index 4bc339f9..43c54bc9 100644 --- a/deny.toml +++ b/deny.toml @@ -79,8 +79,6 @@ ignore = [ # https://github.com/aws/aws-lc-rs/issues/722 { id = "RUSTSEC-2024-0436", reason = "Temporary ignore until aws-lc-rs and rustls-webpki get updated" }, { id = "RUSTSEC-2025-0141", reason = "Temporary ignore until heed migrates to wincode" }, - # ttf-parser (via egui) is unmaintained - { id = "RUSTSEC-2026-0192", reason = "Temporary ignore until ttf-parser is replaced by skrifa etc." }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -120,14 +118,13 @@ exceptions = [ { allow = ["CDLA-Permissive-2.0"], name = "webpki-root-certs", version = "0.26.11" }, { allow = ["CDLA-Permissive-2.0"], name = "webpki-root-certs", version = "1.0.5" }, { allow = ["CDLA-Permissive-2.0"], name = "webpki-roots", version = "1.0.5" }, - { allow = ["LicenseRef-UFL-1.0", "OFL-1.1", "Ubuntu-font-1.0"], name = "epaint_default_fonts", version = "0.34.3" }, { allow = ["MPL-2.0"], name = "bitmaps", version = "3.2.1" }, { allow = ["MPL-2.0"], name = "imbl", version = "7.0.0" }, { allow = ["MPL-2.0"], name = "imbl-sized-chunks", version = "0.1.3" }, { allow = ["MPL-2.0"], name = "option-ext", version = "0.2.0" }, { allow = ["MPL-2.0"], name = "webpki-roots", version = "0.25.4" }, { allow = ["Zlib"], crate = "foldhash" }, - { allow = ["Zlib"], name = "slotmap", version = "1.0.7" }, + { allow = ["Zlib"], name = "slotmap", version = "1.1.1" }, { allow = ["Zlib"], name = "typewit", version = "1.11.0" }, { allow = ["Zlib"], name = "typewit_proc_macros", version = "1.8.1" }, { allow = ["Zlib"], name = "zlib-rs", version = "0.5.5" }, From c5ea088e2d2e979589917b122118c8d21b2abf0e Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 24 Jul 2026 01:15:12 +0800 Subject: [PATCH 02/18] update to latest bip300301_enforcer --- .gitmodules | 3 + Cargo.lock | 241 +++++++------------------- Cargo.toml | 9 +- bip300301_enforcer | 1 + integration_tests/integration_test.rs | 2 +- lib/build.rs | 10 +- lib/types/proto.rs | 7 +- proto | 1 - 8 files changed, 83 insertions(+), 191 deletions(-) create mode 160000 bip300301_enforcer delete mode 160000 proto diff --git a/.gitmodules b/.gitmodules index 76d3c5d4..98d97a17 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "proto"] path = proto url = https://github.com/LayerTwo-Labs/cusf_sidechain_proto.git +[submodule "bip300301_enforcer"] + path = bip300301_enforcer + url = https://github.com/LayerTwo-Labs/bip300301_enforcer.git diff --git a/Cargo.lock b/Cargo.lock index 3f890fd4..aa4f9cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -351,8 +351,6 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", - "hyper-util", "itoa", "matchit", "memchr", @@ -496,7 +494,6 @@ dependencies = [ [[package]] name = "bip300301_enforcer_integration_tests" version = "0.3.4" -source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=1100b6085d9ed64eeaf1b57b3c44609626bd4b23#1100b6085d9ed64eeaf1b57b3c44609626bd4b23" dependencies = [ "anyhow", "bdk_wallet", @@ -513,26 +510,23 @@ dependencies = [ "futures", "hex", "http", - "indicatif", "jsonrpsee 0.26.0", "libtest-mimic", "parking_lot", "reserve-port", + "rusqlite", "serde", "serde_json", "strum 0.28.0", "temp-dir", "thiserror 2.0.18", "tokio", - "tokio-stream", "tracing", - "tracing-indicatif", ] [[package]] name = "bip300301_enforcer_lib" version = "0.3.4" -source = "git+https://github.com/LayerTwo-Labs/bip300301_enforcer?rev=1100b6085d9ed64eeaf1b57b3c44609626bd4b23#1100b6085d9ed64eeaf1b57b3c44609626bd4b23" dependencies = [ "aes-gcm", "argon2", @@ -665,12 +659,12 @@ dependencies = [ [[package]] name = "bitcoin-jsonrpsee" version = "0.1.1" -source = "git+https://github.com/LayerTwo-Labs/bitcoin-jsonrpsee.git?rev=db1fdfd5ecddfc7855cd872546b47429cb29a0bf#db1fdfd5ecddfc7855cd872546b47429cb29a0bf" +source = "git+https://github.com/LayerTwo-Labs/bitcoin-jsonrpsee.git?rev=4810db2112d1bc437b8c4b78520f79e5e9ce8630#4810db2112d1bc437b8c4b78520f79e5e9ce8630" dependencies = [ "base64 0.22.1", "bitcoin", "educe", - "hashlink 0.11.0", + "hashlink 0.12.0", "hex", "http", "jsonrpsee 0.26.0", @@ -789,19 +783,20 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" dependencies = [ "once_cell", "proc-macro-crate", @@ -812,38 +807,39 @@ dependencies = [ [[package]] name = "buffa" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec95898e2ef31d6266042f21c398fa5ff8334d14d6b2ac5fb38b55448d94f595" +checksum = "33f29a40702df4b86ccd84211bfde8cee0bce6d0811450ade4a86a7d0958a23a" dependencies = [ "base64 0.22.1", "bytes", - "compact_str", - "ecow", + "foldhash 0.1.5", "hashbrown 0.15.5", "once_cell", + "rustversion", "serde", "serde_json", - "smol_str 0.3.2", + "smoothutf8", "thiserror 2.0.18", ] [[package]] name = "buffa-descriptor" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f74c0bf128c53f22b0fc3c1e8e66969cd21375879061293fd4e5298e839099a9" +checksum = "57ed423c4ecec86d1500879ce42e7e5f6def01bc632985ac756c1fd723fa21fc" dependencies = [ "buffa", + "rustversion", "serde", "serde_json", ] [[package]] name = "buffa-types" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5103995623b8e213abfa84b89feb98a0df84514607aafd52e6add9ca53c57d2e" +checksum = "22cb54bcbf6946c58e550eeaf6209047146363dbc7cbdc87e49e67089ed2ff5f" dependencies = [ "buffa", "bytes", @@ -950,15 +946,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.54" @@ -1097,7 +1084,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "unicode-width 0.1.14", + "unicode-width 0.2.2", ] [[package]] @@ -1125,21 +1112,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "serde", - "static_assertions", -] - [[package]] name = "compression-codecs" version = "0.4.38" @@ -1170,13 +1142,12 @@ dependencies = [ [[package]] name = "connectrpc" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003fac88ace432fd8e6adbe13da8b20e90613b89e70c72186c505ad1bea307b2" +checksum = "e4d1bbfc95ed1f4f31a027f5b97ff981be2617bc8a5c800fd6fcc701867bb97f" dependencies = [ "async-compression", "async-trait", - "axum", "base64 0.22.1", "buffa", "bytes", @@ -1187,7 +1158,6 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "libc", "percent-encoding", "pin-project", "serde", @@ -1196,7 +1166,6 @@ dependencies = [ "tokio", "tokio-util", "tower", - "tower-http", "tracing", "wasm-bindgen-futures", "zstd", @@ -1426,8 +1395,8 @@ dependencies = [ [[package]] name = "cusf-enforcer-mempool" -version = "0.3.0" -source = "git+https://github.com/LayerTwo-Labs/cusf-enforcer-mempool.git?rev=e6c0ca5620c926c8b3a1df67ac83c45ce9cb4acd#e6c0ca5620c926c8b3a1df67ac83c45ce9cb4acd" +version = "0.4.0" +source = "git+https://github.com/LayerTwo-Labs/cusf-enforcer-mempool.git?rev=cbf0f2057f311f936c8e5e15520da3b4193feb51#cbf0f2057f311f936c8e5e15520da3b4193feb51" dependencies = [ "anyhow", "async-trait", @@ -1437,14 +1406,14 @@ dependencies = [ "educe", "either", "futures", - "hashlink 0.11.0", + "hashlink 0.12.0", "imbl", "indexmap 2.14.0", "jsonrpsee 0.26.0", - "lending-iterator", + "lender", "nonempty 0.12.0", "parking_lot", - "polonius-the-crab 0.5.0", + "polonius-the-crab", "thiserror 2.0.18", "tokio", "tokio-util", @@ -1664,15 +1633,6 @@ dependencies = [ "emath", ] -[[package]] -name = "ecow" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" -dependencies = [ - "serde", -] - [[package]] name = "ed25519" version = "2.2.3" @@ -2049,35 +2009,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "ext-trait" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5" -dependencies = [ - "ext-trait-proc_macros", -] - -[[package]] -name = "ext-trait-proc_macros" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "extension-traits" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537" -dependencies = [ - "ext-trait", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2751,16 +2682,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", - "serde", -] - [[package]] name = "hashlink" version = "0.12.0" @@ -2872,7 +2793,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e690f8474c6c5d8ff99656fcbc195a215acc3949481a8b0b3351c838972dc776" dependencies = [ - "macro_rules_attribute 0.2.2", + "macro_rules_attribute", "never-say-never", "paste", ] @@ -3230,7 +3151,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ "console", - "futures-core", "portable-atomic", "unicode-width 0.2.2", "unit-prefix", @@ -3817,28 +3737,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] -name = "lending-iterator" -version = "0.1.7" +name = "lender" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc07588c853b50689205fb5c00498aa681d89828e0ce8cbd965ebc7a5d8ae260" +checksum = "6e306e6c6b4be049e429a93d121609257648ea8ce31d8d57ede117b85646ecca" dependencies = [ - "extension-traits", - "lending-iterator-proc_macros", - "macro_rules_attribute 0.1.3", - "never-say-never", - "nougat", - "polonius-the-crab 0.2.1", + "aliasable", + "fallible-iterator", + "lender-derive", + "maybe-dangling", + "stable_try_trait_v2", ] [[package]] -name = "lending-iterator-proc_macros" -version = "0.1.7" +name = "lender-derive" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5445dd1c0deb1e97b8a16561d17fc686ca83e8411128fb036e9668a72d51b1d" +checksum = "d074a297c82222d442171bad4f392fef93d35fb31e24a115f605a0c907ce0af9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -4012,32 +3931,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "macro_rules_attribute" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862" -dependencies = [ - "macro_rules_attribute-proc_macro 0.1.3", - "paste", -] - [[package]] name = "macro_rules_attribute" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" dependencies = [ - "macro_rules_attribute-proc_macro 0.2.2", + "macro_rules_attribute-proc_macro", "paste", ] -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d" - [[package]] name = "macro_rules_attribute-proc_macro" version = "0.2.2" @@ -4059,6 +3962,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe-dangling" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59dbb09ed53f8e4f314e353dc6c1853ae5b4c480a668a422657804a544ea9f65" + [[package]] name = "memchr" version = "2.7.6" @@ -4314,27 +4223,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[package]] -name = "nougat" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b57b9ced431322f054fc673f1d3c7fa52d80efd9df74ad2fc759f044742510" -dependencies = [ - "macro_rules_attribute 0.1.3", - "nougat-proc_macros", -] - -[[package]] -name = "nougat-proc_macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84f77a45e99a2f9b492695d99e1c23844619caa5f3e57647cffacad773ca257" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5011,12 +4899,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "polonius-the-crab" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a69ee997a6282f8462abf1e0d8c38c965e968799e912b3bed8c9e8a28c2f9f" - [[package]] name = "polonius-the-crab" version = "0.5.0" @@ -5172,7 +5054,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap", @@ -5352,16 +5234,16 @@ 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]] @@ -6218,7 +6100,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.3.14", + "errno 0.2.8", "libc", ] @@ -6363,13 +6245,12 @@ dependencies = [ ] [[package]] -name = "smol_str" -version = "0.3.2" +name = "smoothutf8" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" +checksum = "36358427d32ecdb1624616deed99eccfef0a167fe5bf40ddb51efe6980bc1ec8" dependencies = [ - "borsh", - "serde", + "simdutf8", ] [[package]] @@ -6446,6 +6327,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stable_try_trait_v2" +version = "1.75.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4e48411f4db8ccca0470bfb67e3bb821af4227d455aa147917d8d109be0d13" + [[package]] name = "static_assertions" version = "1.1.0" @@ -6513,7 +6400,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", - "quote", "unicode-ident", ] @@ -7085,7 +6971,6 @@ dependencies = [ "futures-util", "http", "http-body", - "http-body-util", "iri-string", "pin-project-lite", "tower", @@ -7962,7 +7847,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -8278,7 +8163,7 @@ dependencies = [ "redox_syscall 0.4.1", "rustix 0.38.44", "smithay-client-toolkit 0.19.2", - "smol_str 0.2.2", + "smol_str", "tracing", "unicode-segmentation", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 5ba8b121..18f9994a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] -resolver = "2" +exclude = ["bip300301_enforcer"] members = ["app", "cli", "integration_tests", "lib", "rpc-api"] +resolver = "2" [workspace.package] authors = [ @@ -87,13 +88,11 @@ uuid = "1.13.1" [workspace.dependencies.bip300301_enforcer_lib] default-features = false -git = "https://github.com/LayerTwo-Labs/bip300301_enforcer" -rev = "1100b6085d9ed64eeaf1b57b3c44609626bd4b23" +path = "bip300301_enforcer/lib" [workspace.dependencies.bip300301_enforcer_integration_tests] default-features = false -git = "https://github.com/LayerTwo-Labs/bip300301_enforcer" -rev = "1100b6085d9ed64eeaf1b57b3c44609626bd4b23" +path = "bip300301_enforcer/integration_tests" [workspace.dependencies.l2l-openapi] git = "https://github.com/Ash-L2L/l2l-openapi" diff --git a/bip300301_enforcer b/bip300301_enforcer new file mode 160000 index 00000000..ebd851f1 --- /dev/null +++ b/bip300301_enforcer @@ -0,0 +1 @@ +Subproject commit ebd851f1113dd34e295f487b2368ba397da8abba diff --git a/integration_tests/integration_test.rs b/integration_tests/integration_test.rs index 16acd6ff..e0383f87 100644 --- a/integration_tests/integration_test.rs +++ b/integration_tests/integration_test.rs @@ -55,7 +55,7 @@ pub async fn deposit_withdraw_roundtrip_task( tracing::info!("Deposited to sidechain successfully"); // Wait for mempool to catch up before attempting second deposit tracing::debug!("Waiting for wallet sync..."); - let () = wait_for_wallet_sync().await?; + let () = wait_for_wallet_sync(post_setup).await?; tracing::info!("Attempting second deposit"); let () = deposit( post_setup, diff --git a/lib/build.rs b/lib/build.rs index 9008d6e5..d34f0f15 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -29,12 +29,14 @@ where } fn main() -> Result<(), Box> { - const COMMON_PROTO: &str = "../proto/proto/cusf/common/v1/common.proto"; + const COMMON_PROTO: &str = + "../bip300301_enforcer/proto/cusf/common/v1/common.proto"; const VALIDATOR_PROTO: &str = - "../proto/proto/cusf/mainchain/v1/validator.proto"; - const WALLET_PROTO: &str = "../proto/proto/cusf/mainchain/v1/wallet.proto"; + "../bip300301_enforcer/proto/cusf/mainchain/v1/validator.proto"; + const WALLET_PROTO: &str = + "../bip300301_enforcer/proto/cusf/mainchain/v1/wallet.proto"; const ALL_PROTOS: &[&str] = &[COMMON_PROTO, VALIDATOR_PROTO, WALLET_PROTO]; - const INCLUDES: &[&str] = &["../proto/proto"]; + const INCLUDES: &[&str] = &["../bip300301_enforcer/proto"]; let file_descriptors = protox::compile(ALL_PROTOS, INCLUDES)?; let file_descriptor_path = PathBuf::from( env::var("OUT_DIR").expect("OUT_DIR environment variable not set"), diff --git a/lib/types/proto.rs b/lib/types/proto.rs index 156ce851..bc52c3e6 100644 --- a/lib/types/proto.rs +++ b/lib/types/proto.rs @@ -835,6 +835,7 @@ pub mod mainchain { prev_block_hash, height, work, + timestamp: _, } = header_info; let block_hash = block_hash .as_ref() @@ -1103,8 +1104,10 @@ pub mod mainchain { &mut self, ) -> Result { let request = generated::GetChainInfoRequest {}; - let generated::GetChainInfoResponse { network } = - self.0.get_chain_info(request).await?.into_inner(); + let generated::GetChainInfoResponse { + network, + bip300_constants: _, + } = self.0.get_chain_info(request).await?.into_inner(); let network = generated::Network::try_from(network) .map_err(|_| super::Error::UnknownEnumTag { field_name: "network".to_owned(), diff --git a/proto b/proto deleted file mode 160000 index 0874a1d2..00000000 --- a/proto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0874a1d2a005d962bf1f82c19a690a255891b8e4 From 8a6b7328787fc3d75eba8d19e8684fefe5eb90e7 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 24 Jul 2026 01:51:59 +0800 Subject: [PATCH 03/18] Bump version for release 0.17.0 --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aa4f9cd6..39c20a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6512,7 +6512,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.16.1" +version = "0.17.0" dependencies = [ "anyhow", "async-lock", @@ -6569,7 +6569,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.16.1" +version = "0.17.0" dependencies = [ "anyhow", "bincode", @@ -6611,7 +6611,7 @@ dependencies = [ [[package]] name = "thunder_app_cli" -version = "0.16.1" +version = "0.17.0" dependencies = [ "anyhow", "bitcoin", @@ -6633,7 +6633,7 @@ dependencies = [ [[package]] name = "thunder_app_rpc_api" -version = "0.16.1" +version = "0.17.0" dependencies = [ "bitcoin", "jsonrpsee 0.25.1", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "thunder_integration_tests" -version = "0.16.1" +version = "0.17.0" dependencies = [ "anyhow", "bip300301_enforcer_integration_tests", diff --git a/Cargo.toml b/Cargo.toml index 18f9994a..dc8fb1bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ edition = "2024" license-file = "LICENSE.txt" publish = false -version = "0.16.1" +version = "0.17.0" [workspace.dependencies] anyhow = "1.0.72" From db566b4f808ed688067333a5b213aadb7610aba0 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 24 Jul 2026 17:29:09 +0800 Subject: [PATCH 04/18] refactor: init types crate --- Cargo.lock | 49 +- Cargo.toml | 2 +- app/rpc_server.rs | 6 +- lib/Cargo.toml | 24 +- lib/lib.rs | 1 - lib/net/mod.rs | 9 +- lib/net/peer/mod.rs | 102 +-- lib/node/mod.rs | 3 +- lib/state/block.rs | 32 +- lib/state/mod.rs | 17 +- lib/types/mod.rs | 1112 +-------------------------- lib/wallet.rs | 40 +- rpc-api/Cargo.toml | 2 +- rpc-api/lib.rs | 24 +- types/Cargo.toml | 45 ++ {lib/types => types}/address.rs | 14 +- {lib => types}/authorization.rs | 53 +- types/error.rs | 52 ++ {lib/types => types}/hashes.rs | 10 +- types/lib.rs | 1033 +++++++++++++++++++++++++ types/net.rs | 29 + {lib/types => types}/schema.rs | 0 {lib/types => types}/transaction.rs | 57 +- types/util.rs | 112 +++ types/wallet.rs | 16 + 25 files changed, 1489 insertions(+), 1355 deletions(-) create mode 100644 types/Cargo.toml rename {lib/types => types}/address.rs (86%) rename {lib => types}/authorization.rs (81%) create mode 100644 types/error.rs rename {lib/types => types}/hashes.rs (94%) create mode 100644 types/lib.rs create mode 100644 types/net.rs rename {lib/types => types}/schema.rs (100%) rename {lib/types => types}/transaction.rs (93%) create mode 100644 types/util.rs create mode 100644 types/wallet.rs diff --git a/Cargo.lock b/Cargo.lock index 39c20a17..c2688316 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -742,16 +742,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]] @@ -5054,7 +5054,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -6100,7 +6100,7 @@ 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", ] @@ -6518,11 +6518,9 @@ dependencies = [ "async-lock", "bincode", "bitcoin", - "blake3", "borsh", "byteorder", "bytes", - "clap", "ed25519-dalek", "ed25519-dalek-bip32", "educe", @@ -6534,7 +6532,6 @@ dependencies = [ "heed", "hex", "jsonrpsee 0.25.1", - "merkle-cbt", "nonempty 0.11.0", "parking_lot", "prost", @@ -6545,16 +6542,13 @@ dependencies = [ "rayon", "rcgen", "rustls", - "rustreexo", "semver", "serde", "serde_json", - "serde_with", - "smallvec", "sneed", - "strum 0.27.2", "temp-dir", "thiserror 2.0.18", + "thunder_types", "tiny-bip39", "tokio", "tokio-stream", @@ -6564,7 +6558,6 @@ dependencies = [ "tonic-prost-build", "tracing", "transitive", - "utoipa", ] [[package]] @@ -6640,7 +6633,7 @@ dependencies = [ "l2l-openapi", "serde", "serde_json", - "thunder", + "thunder_types", "utoipa", ] @@ -6667,6 +6660,32 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "thunder_types" +version = "0.17.0" +dependencies = [ + "anyhow", + "bitcoin", + "blake3", + "borsh", + "clap", + "ed25519-dalek", + "hashlink 0.10.0", + "heed", + "hex", + "merkle-cbt", + "rayon", + "rustreexo", + "semver", + "serde", + "serde_with", + "smallvec", + "strum 0.27.2", + "thiserror 2.0.18", + "tracing", + "utoipa", +] + [[package]] name = "tiff" version = "0.10.3" @@ -7847,7 +7866,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dc8fb1bc..2be7b983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] exclude = ["bip300301_enforcer"] -members = ["app", "cli", "integration_tests", "lib", "rpc-api"] +members = ["app", "cli", "integration_tests", "lib", "rpc-api", "types"] resolver = "2" [workspace.package] diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 29f38fe0..6eb97f04 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -6,10 +6,8 @@ use jsonrpsee::{ server::Server, types::ErrorObject, }; -use thunder::{ - net::Peer, - types::{Address, PointedOutput, Txid, WithdrawalBundle}, - wallet::Balance, +use thunder::types::{ + Address, PointedOutput, Txid, WithdrawalBundle, net::Peer, wallet::Balance, }; use thunder_app_rpc_api::{GetTransactionResponse, RpcServer}; use tower_http::{ diff --git a/lib/Cargo.toml b/lib/Cargo.toml index dcb3a8ce..81f3abbd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -13,16 +13,14 @@ protox = { workspace = true } tonic-prost-build = { workspace = true } [dependencies] -anyhow = { workspace = true, features = ["backtrace"] } +anyhow = { workspace = true } async-lock = { workspace = true } bincode = { workspace = true } -bitcoin = { workspace = true, features = ["serde"] } -blake3 = { workspace = true } -borsh = { workspace = true, features = ["derive"] } +bitcoin = { workspace = true } +borsh = { workspace = true } byteorder = { workspace = true } bytes = { workspace = true } -clap = { workspace = true, features = ["derive"], optional = true } -ed25519-dalek = { workspace = true, features = ["batch", "serde"] } +ed25519-dalek = { workspace = true } ed25519-dalek-bip32 = { workspace = true } educe = { workspace = true, features = ["Clone", "Debug"] } error-fatality = { workspace = true } @@ -31,9 +29,8 @@ futures = { workspace = true, features = ["alloc"] } governor = { workspace = true, features = ["quanta", "std"] } hashlink = { workspace = true, features = ["serde_impl"] } heed = { workspace = true } -hex = { workspace = true, features = ["serde"] } +hex = { workspace = true } jsonrpsee = { workspace = true } -merkle-cbt = { workspace = true } nonempty = { workspace = true, features = ["serialize"] } parking_lot = { workspace = true } prost = { workspace = true } @@ -42,15 +39,12 @@ quinn = { workspace = true } rayon = { workspace = true } rcgen = { workspace = true } rustls = { workspace = true, features = ["ring"] } -rustreexo = { workspace = true, features = ["with-serde"] } -smallvec = { workspace = true, features = ["write"] } -semver = { workspace = true, features = ["serde"] } +semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -serde_with = { workspace = true } sneed = { workspace = true, features = ["observe"] } -strum = { workspace = true } thiserror = { workspace = true } +thunder_types = { path = "../types", features = ["heed"] } tiny-bip39 = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } tokio-stream = { workspace = true, features = ["sync"] } @@ -59,13 +53,13 @@ tonic = { workspace = true } tonic-prost = { workspace = true } tracing = { workspace = true } transitive = { workspace = true } -utoipa = { workspace = true, features = ["macros", "non_strict_integers"] } [dev-dependencies] +hashlink = { workspace = true } temp-dir = { workspace = true } [features] -clap = ["dep:clap"] +clap = ["thunder_types/clap"] [lints] workspace = true diff --git a/lib/lib.rs b/lib/lib.rs index 2d3002ee..8e283433 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -2,7 +2,6 @@ #![feature(trait_alias)] pub mod archive; -pub mod authorization; pub mod mempool; pub mod miner; pub mod net; diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 8a317759..93d1e5f8 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -19,7 +19,10 @@ use tracing::instrument; use crate::{ archive::Archive, state::State, - types::{AuthorizedTransaction, Network, THIS_SIDECHAIN, VERSION, Version}, + types::{ + AuthorizedTransaction, Network, THIS_SIDECHAIN, VERSION, Version, + net::{Peer, PeerConnectionStatus}, + }, }; pub mod error; @@ -33,8 +36,8 @@ use peer::{ }; pub use peer::{ ConnectionError as PeerConnectionError, Info as PeerConnectionInfo, - InternalMessage as PeerConnectionMessage, Peer, PeerConnectionStatus, - PeerStateId, Request as PeerRequest, ResponseMessage as PeerResponse, + InternalMessage as PeerConnectionMessage, PeerStateId, + Request as PeerRequest, ResponseMessage as PeerResponse, message as peer_message, }; diff --git a/lib/net/peer/mod.rs b/lib/net/peer/mod.rs index 4299edc2..938e41b5 100644 --- a/lib/net/peer/mod.rs +++ b/lib/net/peer/mod.rs @@ -18,7 +18,10 @@ use tokio::{spawn, task::JoinHandle, time::Duration}; use crate::{ archive::Archive, state::State, - types::{AuthorizedTransaction, Hash, Network, Tip, Version, hash, schema}, + types::{ + AuthorizedTransaction, Hash, Network, Tip, Version, hash, + net::PeerConnectionStatus, + }, }; mod channel_pool; @@ -371,39 +374,56 @@ pub struct ConnectionContext { pub state: State, } -#[derive( - Clone, - Copy, - Eq, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - utoipa::ToSchema, -)] -pub enum PeerConnectionStatus { - /// We're still in the process of initializing the peer connection - Connecting, - /// The connection is successfully established - Connected, -} +/// Used to make `bool` representation explicit and unique +#[repr(transparent)] +struct StatusRepr(bool); -impl PeerConnectionStatus { - /// Convert from boolean representation - // Should remain private to this module - fn from_repr(repr: bool) -> Self { +impl From for PeerConnectionStatus { + fn from(repr: StatusRepr) -> Self { + let StatusRepr(repr) = repr; match repr { false => Self::Connecting, true => Self::Connected, } } +} + +impl From for StatusRepr { + fn from(status: PeerConnectionStatus) -> Self { + match status { + PeerConnectionStatus::Connecting => Self(false), + PeerConnectionStatus::Connected => Self(true), + } + } +} + +/// Atomic representation of [`PeerConnectionStatus`] +#[repr(transparent)] +pub(in crate::net) struct AtomicStatus { + atomic_repr: AtomicBool, +} + +impl AtomicStatus { + #[inline(always)] + fn load(&self, ordering: atomic::Ordering) -> StatusRepr { + StatusRepr(self.atomic_repr.load(ordering)) + } + + #[inline(always)] + fn store(&self, repr: StatusRepr, ordering: atomic::Ordering) { + self.atomic_repr.store(repr.0, ordering) + } +} - /// Convert to boolean representation - // Should remain private to this module - fn as_repr(self) -> bool { - match self { - Self::Connecting => false, - Self::Connected => true, +impl From for AtomicStatus +where + StatusRepr: From, +{ + #[inline(always)] + fn from(value: T) -> Self { + let StatusRepr(repr) = value.into(); + Self { + atomic_repr: AtomicBool::new(repr), } } } @@ -413,17 +433,15 @@ pub struct ConnectionHandle { task: JoinHandle<()>, /// Indicates that at least one message has been received successfully pub(in crate::net) received_msg_successfully: Arc, - /// Representation of [`PeerConnectionStatus`] - pub(in crate::net) status_repr: Arc, + /// Atomic representation of [`PeerConnectionStatus`] + pub(in crate::net) status: Arc, /// Push messages from connection task / net task / node pub internal_message_tx: mpsc::UnboundedSender, } impl ConnectionHandle { pub fn connection_status(&self) -> PeerConnectionStatus { - PeerConnectionStatus::from_repr( - self.status_repr.load(atomic::Ordering::SeqCst), - ) + self.status.load(atomic::Ordering::SeqCst).into() } /// Indicates that at least one message has been received successfully @@ -480,7 +498,7 @@ pub fn handle( let connection_handle = ConnectionHandle { task, received_msg_successfully, - status_repr: Arc::new(AtomicBool::new(status.as_repr())), + status: Arc::new(AtomicStatus::from(status)), internal_message_tx, }; (connection_handle, info_rx) @@ -491,20 +509,20 @@ pub fn connect( ctxt: ConnectionContext, ) -> (ConnectionHandle, mpsc::UnboundedReceiver) { let connection_status = PeerConnectionStatus::Connecting; - let status_repr = Arc::new(AtomicBool::new(connection_status.as_repr())); + let status = Arc::new(AtomicStatus::from(connection_status)); let received_msg_successfully = Arc::new(AtomicBool::new(false)); let (info_tx, info_rx) = mpsc::unbounded(); let (mailbox_tx, mailbox_rx) = mailbox::new(); let internal_message_tx = mailbox_tx.internal_message_tx.clone(); let connection_task = { let received_msg_successfully = received_msg_successfully.clone(); - let status_repr = status_repr.clone(); + let status = status.clone(); let info_tx = info_tx.clone(); move || async move { let connection = Connection::from_connecting(connecting, ctxt.network).await?; - status_repr.store( - PeerConnectionStatus::Connected.as_repr(), + status.store( + PeerConnectionStatus::Connected.into(), atomic::Ordering::SeqCst, ); @@ -530,20 +548,12 @@ pub fn connect( let connection_handle = ConnectionHandle { task, received_msg_successfully, - status_repr, + status, internal_message_tx, }; (connection_handle, info_rx) } -// RPC output representation for peer + state -#[derive(Clone, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] -pub struct Peer { - #[schema(value_type = schema::SocketAddr)] - pub address: SocketAddr, - pub status: PeerConnectionStatus, -} - #[cfg(test)] mod test { use std::num::NonZeroUsize; diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 26a58a2f..04bda213 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -16,13 +16,14 @@ use tonic::transport::Channel; use crate::{ archive::{self, Archive}, mempool::{self, MemPool}, - net::{self, Net, Peer}, + net::{self, Net}, state::{self, State}, types::{ Accumulator, Address, AmountOverflowError, AmountUnderflowError, Authorized, AuthorizedTransaction, BlockHash, BmmResult, Body, FilledTransaction, GetValue, Header, Network, OutPoint, OutPointKey, Output, SpentOutput, Tip, Transaction, Txid, WithdrawalBundle, + net::Peer, proto::{self, mainchain}, }, util::Watchable, diff --git a/lib/state/block.rs b/lib/state/block.rs index 82b1b2cb..4af031e6 100644 --- a/lib/state/block.rs +++ b/lib/state/block.rs @@ -1,15 +1,14 @@ //! Connect and disconnect blocks -use rustreexo::accumulator::node_hash::BitcoinNodeHash; use sneed::{RoTxn, RwTxn, db::error::Error as DbError}; use crate::{ - authorization::Authorization, state::{Error, PrevalidatedBlock, State, error}, types::{ - AccumulatorDiff, AmountOverflowError, Body, FilledTransaction, - GetAddress as _, GetValue as _, Header, InPoint, MerkleRoot, OutPoint, - OutPointKey, PointedOutput, SpentOutput, Verify as _, + AccumulatorDiff, AmountOverflowError, Authorization, Body, + FilledTransaction, GetAddress as _, GetValue as _, Header, InPoint, + MerkleRoot, OutPoint, OutPointKey, PointedOutput, SpentOutput, + UtreexoNodeHash, Verify as _, }, }; @@ -64,7 +63,7 @@ pub fn prevalidate( let txid = transaction.txid(); let mut spent_utxos = Vec::with_capacity(transaction.inputs.len()); let mut spent_utxo_hashes = - Vec::::with_capacity(transaction.inputs.len()); + Vec::::with_capacity(transaction.inputs.len()); for (outpoint, utxo_hash) in &transaction.inputs { let key = OutPointKey::from(outpoint); let spent_output = @@ -160,7 +159,7 @@ pub fn prevalidate( } // Check root consistency without committing to DB let () = accumulator.apply_diff(accumulator_diff.clone())?; - let roots: Vec = accumulator.get_roots(); + let roots: Vec = accumulator.get_roots(); if roots != header.roots { return Err(Error::UtreexoRootsMismatch); } @@ -343,7 +342,7 @@ pub fn validate( for filled_transaction in &filled_transactions { let txid = filled_transaction.transaction.txid(); // hashes of spent utxos, used to verify the utreexo proof - let mut spent_utxo_hashes = Vec::::with_capacity( + let mut spent_utxo_hashes = Vec::::with_capacity( filled_transaction.transaction.inputs.len(), ); for (outpoint, utxo_hash) in &filled_transaction.transaction.inputs { @@ -400,7 +399,7 @@ pub fn validate( } // Check root consistency without committing to DB let () = accumulator.apply_diff(accumulator_diff)?; - let roots: Vec = accumulator.get_roots(); + let roots: Vec = accumulator.get_roots(); if roots != header.roots { return Err(Error::UtreexoRootsMismatch); } @@ -631,15 +630,14 @@ mod test { #[test] fn validation_rejects_outpoint_utxo_hash_mismatch() -> anyhow::Result<()> { use bitcoin::hashes::Hash as _; - use rustreexo::accumulator::node_hash::BitcoinNodeHash; - use crate::{ + use crate::types::{ + Accumulator, AccumulatorDiff, Body, Header, OutPoint, OutPointKey, + PointedOutput, Transaction, UtreexoNodeHash, authorization::{SigningKey, authorize, get_address}, - types::{ - Accumulator, AccumulatorDiff, Body, Header, OutPoint, - OutPointKey, PointedOutput, Transaction, hash, - }, + hash, }; + let (_temp_dir, env, state) = fresh_state("validation_rejects_outpoint_utxo_hash_mismatch")?; @@ -670,8 +668,8 @@ mod test { outpoint: outpoint_b, output: output_b.clone(), }; - let leaf_a: BitcoinNodeHash = (&pointed_a).into(); - let leaf_b: BitcoinNodeHash = (&pointed_b).into(); + let leaf_a: UtreexoNodeHash = (&pointed_a).into(); + let leaf_b: UtreexoNodeHash = (&pointed_b).into(); let hash_b: crate::types::Hash = hash(&pointed_b); // input's utxo_hash // Helper: build a fresh accumulator seeded with leaves A and B diff --git a/lib/state/mod.rs b/lib/state/mod.rs index eb5bbdca..47920e43 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use fallible_iterator::FallibleIterator as _; use futures::Stream; use heed::types::SerdeBincode; -use rustreexo::accumulator::{node_hash::BitcoinNodeHash, proof::Proof}; use serde::{Deserialize, Serialize}; use sneed::{ DatabaseUnique, RoTxn, RwTxn, UnitKey, @@ -13,13 +12,13 @@ use sneed::{ }; use crate::{ - authorization::Authorization, types::{ Accumulator, Address, AmountOverflowError, AmountUnderflowError, - Authorized, AuthorizedTransaction, BlockHash, Body, FilledTransaction, - GetAddress, GetValue, Header, InPoint, M6id, MerkleRoot, OutPoint, - OutPointKey, Output, PointedOutput, PointedOutputRef, SpentOutput, - Transaction, VERSION, Verify, Version, WithdrawalBundle, + Authorization, Authorized, AuthorizedTransaction, BlockHash, Body, + FilledTransaction, GetAddress, GetValue, Header, InPoint, M6id, + MerkleRoot, OutPoint, OutPointKey, Output, PointedOutput, + PointedOutputRef, SpentOutput, Transaction, UtreexoNodeHash, + UtreexoProof, VERSION, Verify, Version, WithdrawalBundle, WithdrawalBundleStatus, proto::mainchain::TwoWayPegData, }, util::Watchable, @@ -265,13 +264,13 @@ impl State { &self, rotxn: &RoTxn, utxos: Utxos, - ) -> Result + ) -> Result where Utxos: IntoIterator, { let accumulator = self.get_accumulator(rotxn)?; - let targets: Vec = - utxos.into_iter().map(BitcoinNodeHash::from).collect(); + let targets: Vec = + utxos.into_iter().map(UtreexoNodeHash::from).collect(); let proof = accumulator.prove(&targets)?; Ok(proof) } diff --git a/lib/types/mod.rs b/lib/types/mod.rs index 0fbd6a9d..22b5072a 100644 --- a/lib/types/mod.rs +++ b/lib/types/mod.rs @@ -1,1117 +1,13 @@ -use borsh::BorshSerialize; -use hashlink::{LinkedHashMap, linked_hash_map}; -use rustreexo::accumulator::{ - mem_forest::MemForest, node_hash::BitcoinNodeHash, proof::Proof, -}; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; -use std::{ - borrow::Borrow, - cmp::Ordering, - collections::{BTreeMap, HashMap}, - sync::LazyLock, -}; -use thiserror::Error; -use utoipa::ToSchema; +//! Mostly a wrapper around plain_bitnames_types -use crate::{ - authorization::Authorization, types::transaction::ComputeFeeError, -}; +use std::sync::LazyLock; -mod address; -pub mod hashes; -pub mod proto; -pub mod schema; -mod transaction; - -pub use address::Address; -pub use hashes::{ - BlockHash, Hash, M6id, MerkleRoot, Txid, hash, hash_with_scratch_buffer, -}; -pub use transaction::{ - Authorized, AuthorizedTransaction, Content as OutputContent, - FilledTransaction, GetAddress, GetValue, InPoint, OutPoint, OutPointKey, - Output, PointedOutput, PointedOutputRef, SpentOutput, Transaction, -}; - -pub const THIS_SIDECHAIN: u8 = 9; - -#[derive(Debug, Error)] -#[error("Bitcoin amount overflow")] -pub struct AmountOverflowError; - -#[derive(Debug, Error)] -#[error("Bitcoin amount underflow")] -pub struct AmountUnderflowError; - -/// (de)serialize as hex strings for human-readable forms like json, -/// and default serialization for non human-readable formats like bincode -mod serde_hexstr_human_readable { - use hex::{FromHex, ToHex}; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - pub fn serialize(data: T, serializer: S) -> Result - where - S: Serializer, - T: Serialize + ToHex, - { - if serializer.is_human_readable() { - hex::serde::serialize(data, serializer) - } else { - data.serialize(serializer) - } - } - - pub fn deserialize<'de, D, T>(deserializer: D) -> Result - where - D: Deserializer<'de>, - T: Deserialize<'de> + FromHex, - ::Error: std::fmt::Display, - { - if deserializer.is_human_readable() { - hex::serde::deserialize(deserializer) - } else { - T::deserialize(deserializer) - } - } -} - -fn borsh_serialize_utreexo_nodehash( - node_hash: &BitcoinNodeHash, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - let bytes: &[u8; 32] = node_hash; - borsh::BorshSerialize::serialize(bytes, writer) -} - -fn borsh_serialize_utreexo_roots( - roots: &[BitcoinNodeHash], - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - #[derive(BorshSerialize)] - #[repr(transparent)] - struct SerializeBitcoinNodeHash<'a>( - #[borsh(serialize_with = "borsh_serialize_utreexo_nodehash")] - &'a BitcoinNodeHash, - ); - let roots: Vec = - roots.iter().map(SerializeBitcoinNodeHash).collect(); - borsh::BorshSerialize::serialize(&roots, writer) -} - -fn borsh_serialize_bitcoin_block_hash( - block_hash: &bitcoin::BlockHash, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - let bytes: &[u8; 32] = block_hash.as_ref(); - borsh::BorshSerialize::serialize(bytes, writer) -} - -#[derive( - BorshSerialize, - Clone, - Debug, - Deserialize, - Eq, - Hash, - PartialEq, - Serialize, - ToSchema, -)] -pub struct Header { - pub merkle_root: MerkleRoot, - pub prev_side_hash: Option, - #[borsh(serialize_with = "borsh_serialize_bitcoin_block_hash")] - #[schema(value_type = schema::BitcoinBlockHash)] - pub prev_main_hash: bitcoin::BlockHash, - /// Utreexo roots - #[borsh(serialize_with = "borsh_serialize_utreexo_roots")] - #[schema(value_type = Vec)] - pub roots: Vec, -} - -impl Header { - pub fn hash(&self) -> BlockHash { - hash_with_scratch_buffer(self).into() - } -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum WithdrawalBundleEventStatus { - Confirmed, - Failed, - Submitted, -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum WithdrawalBundleStatus { - Confirmed, - /// Formerly pending bundle - Dropped, - Failed, - Pending, - Submitted, - /// Submitted, but unexpected due to previously being dropped or failing. - /// It may not be possible to account for this withdrawal bundle, if it - /// double-spends UTXOs. - SubmittedUnexpected, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct WithdrawalBundleEvent { - pub m6id: M6id, - pub status: WithdrawalBundleEventStatus, -} - -pub static OP_DRIVECHAIN_SCRIPT: LazyLock = - LazyLock::new(|| { - let mut script = bitcoin::ScriptBuf::new(); - script.push_opcode(bitcoin::opcodes::all::OP_RETURN); - script.push_instruction(bitcoin::script::Instruction::PushBytes( - &bitcoin::script::PushBytesBuf::from([THIS_SIDECHAIN]), - )); - script.push_opcode(bitcoin::opcodes::OP_TRUE); - script - }); - -#[derive(Debug, Error)] -enum WithdrawalBundleErrorInner { - #[error("bundle too heavy: weight `{weight}` > max weight `{max_weight}`")] - BundleTooHeavy { weight: u64, max_weight: u64 }, -} - -#[derive(Debug, Error)] -#[error("Withdrawal bundle error")] -pub struct WithdrawalBundleError(#[from] WithdrawalBundleErrorInner); - -#[serde_as] -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] -pub struct WithdrawalBundle { - #[schema(value_type = Vec<(transaction::OutPoint, transaction::Output)>)] - #[serde_as(as = "serde_with::IfIsHumanReadable>")] - spend_utxos: BTreeMap, - #[schema(value_type = schema::BitcoinTransaction)] - tx: bitcoin::Transaction, -} - -impl WithdrawalBundle { - /// Compute the size of a single txout - pub const fn txout_size(spk_size: u32) -> Option { - let Some(size) = (bitcoin::Amount::SIZE as u32) - .checked_add(bitcoin::VarInt(spk_size as u64).size() as u32) - else { - return None; - }; - size.checked_add(spk_size) - } - - /// Predict the weight of a withdrawal bundle, based on the number of - /// outputs (not including the commitment/treasury outputs) and the - /// sum of sizes of txouts (not including the commitment/treasury outputs). - /// Returns None if the predicted weight exceeds the maximum tx weight. - pub const fn predict_weight( - n_outputs: u32, - sum_txout_sizes: u32, - ) -> Option { - use bitcoin::{VarInt, Weight}; - const fn txin_base_size(script_sig_size: u32) -> Option { - const OUTPOINT_SIZE: u8 = 36; - const SEQUENCE_SIZE: u8 = 4; - let script_sig_len_size: u8 = - VarInt(script_sig_size as u64).size() as u8; - let Some(res) = ((OUTPOINT_SIZE + script_sig_len_size) as u32) - .checked_add(script_sig_size) - else { - return None; - }; - res.checked_add(SEQUENCE_SIZE as u32) - } - const fn tx_base_size( - n_inputs: u32, - sum_txin_base_sizes: u32, - n_outputs: u32, - sum_txout_sizes: u32, - ) -> Option { - const VERSION_SIZE: u8 = 4; - const fn vin_base_size( - n_inputs: u32, - sum_txin_base_sizes: u32, - ) -> Option { - let len_size = VarInt(n_inputs as u64).size() as u8; - (len_size as u32).checked_add(sum_txin_base_sizes) - } - const fn vout_size( - n_outputs: u32, - sum_txout_sizes: u32, - ) -> Option { - let len_size = VarInt(n_outputs as u64).size() as u8; - (len_size as u32).checked_add(sum_txout_sizes) - } - const LOCKTIME_SIZE: u8 = bitcoin::absolute::LockTime::SIZE as u8; - let res = VERSION_SIZE as u32; - let Some(vin_base_size) = - vin_base_size(n_inputs, sum_txin_base_sizes) - else { - return None; - }; - let Some(res) = res.checked_add(vin_base_size) else { - return None; - }; - let Some(vout_size) = vout_size(n_outputs, sum_txout_sizes) else { - return None; - }; - let Some(res) = res.checked_add(vout_size) else { - return None; - }; - res.checked_add(LOCKTIME_SIZE as u32) - } - const N_INPUTS: u32 = 1; - const SUM_TXIN_BASE_SIZES: u32 = { - const TREASURY_TXIN_BASE_SIZE: u32 = { - const TREASURY_SCRIPT_SIG_SIZE: u32 = 0; - txin_base_size(TREASURY_SCRIPT_SIG_SIZE).unwrap() - }; - TREASURY_TXIN_BASE_SIZE - }; - let Some(n_outputs) = n_outputs.checked_add(2) else { - return None; - }; - let Some(sum_txout_sizes) = ({ - const INPUTS_COMMITMENT_TXOUT_SIZE: u32 = { - const INPUTS_COMMITMENT_OUTPUT_SPK_SIZE: u8 = 34; - WithdrawalBundle::txout_size( - INPUTS_COMMITMENT_OUTPUT_SPK_SIZE as u32, - ) - .unwrap() - }; - const MAINCHAIN_FEE_COMMITMENT_TXOUT_SIZE: u32 = { - const MAINCHAIN_FEE_COMMITMENT_OUTPUT_SPK_SIZE: u8 = 10; - WithdrawalBundle::txout_size( - MAINCHAIN_FEE_COMMITMENT_OUTPUT_SPK_SIZE as u32, - ) - .unwrap() - }; - (INPUTS_COMMITMENT_TXOUT_SIZE + MAINCHAIN_FEE_COMMITMENT_TXOUT_SIZE) - .checked_add(sum_txout_sizes) - }) else { - return None; - }; - let Some(tx_base_size) = tx_base_size( - N_INPUTS, - SUM_TXIN_BASE_SIZES, - n_outputs, - sum_txout_sizes, - ) else { - return None; - }; - let Some(tx_weight_wu) = - (tx_base_size as u64).checked_mul(Weight::WITNESS_SCALE_FACTOR) - else { - return None; - }; - if tx_weight_wu <= bitcoin::Transaction::MAX_STANDARD_WEIGHT.to_wu() { - Some(Weight::from_wu(tx_weight_wu)) - } else { - None - } - } - - pub fn new( - block_height: u32, - fee: bitcoin::Amount, - 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 = hash(&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, - } - }; - let mainchain_fee_txout = { - let script_pubkey = bitcoin::script::Builder::new() - .push_opcode(bitcoin::opcodes::all::OP_RETURN) - .push_slice(fee.to_sat().to_be_bytes()) - .into_script(); - bitcoin::TxOut { - value: bitcoin::Amount::ZERO, - script_pubkey, - } - }; - let outputs = Vec::from_iter( - [mainchain_fee_txout, inputs_commitment_txout] - .into_iter() - .chain(bundle_outputs), - ); - let tx = bitcoin::Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::blockdata::locktime::absolute::LockTime::ZERO, - input: Vec::new(), - output: outputs, - }; - if tx.weight().to_wu() > bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 - { - Err(WithdrawalBundleErrorInner::BundleTooHeavy { - weight: tx.weight().to_wu(), - max_weight: bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64, - })?; - } - Ok(Self { spend_utxos, tx }) - } - - pub fn compute_m6id(&self) -> M6id { - M6id(self.tx.compute_txid()) - } - - pub fn spend_utxos( - &self, - ) -> &BTreeMap { - &self.spend_utxos - } - - pub fn tx(&self) -> &bitcoin::Transaction { - &self.tx - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct TwoWayPegData { - pub deposits: HashMap, - pub deposit_block_hash: Option, - pub bundle_statuses: HashMap, -} - -/* -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct DisconnectData { - pub spent_utxos: HashMap, - pub deposits: Vec, - pub pending_bundles: Vec, - pub spent_bundles: HashMap>, - pub spent_withdrawals: HashMap, - pub failed_withdrawals: Vec, -} -*/ - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct AggregatedWithdrawal { - pub spend_utxos: HashMap, - pub main_address: bitcoin::Address, - pub value: bitcoin::Amount, - pub main_fee: bitcoin::Amount, -} - -impl Ord for AggregatedWithdrawal { - fn cmp(&self, other: &Self) -> Ordering { - // A *total* order (lexicographic by main_fee, value, main_address). The - // previous `OR of >` was not antisymmetric/transitive, so the - // withdrawal-bundle output order (and hence compute_m6id) depended on - // HashMap iteration order and could differ across nodes. A real total order makes - // the sorted bundle canonical regardless of aggregation order. - (self.main_fee, self.value, &self.main_address).cmp(&( - other.main_fee, - other.value, - &other.main_address, - )) - } -} - -impl PartialOrd for AggregatedWithdrawal { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// 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)] -pub struct AccumulatorDiff { - /// `true` indicates insertion, `false` indicates removal. - diff: LinkedHashMap, - /// Total number of insertions still represented in `diff`. - insertions: usize, - /// Total number of deletions still represented in `diff`. - deletions: usize, -} - -impl Default for AccumulatorDiff { - fn default() -> Self { - Self { - diff: LinkedHashMap::new(), - insertions: 0, - deletions: 0, - } - } -} - -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: BitcoinNodeHash) { - match self.diff.entry(utxo_hash) { - linked_hash_map::Entry::Occupied(entry) => { - if !entry.get() { - entry.remove(); - debug_assert!(self.deletions > 0); - self.deletions -= 1; - } - } - linked_hash_map::Entry::Vacant(entry) => { - entry.insert(true); - self.insertions += 1; - } - } - } - - pub fn remove(&mut self, utxo_hash: BitcoinNodeHash) { - match self.diff.entry(utxo_hash) { - linked_hash_map::Entry::Occupied(entry) => { - if *entry.get() { - entry.remove(); - debug_assert!(self.insertions > 0); - self.insertions -= 1; - } - } - linked_hash_map::Entry::Vacant(entry) => { - entry.insert(false); - self.deletions += 1; - } - } - } - - /// Returns the number of tracked insertions and deletions, in that order. - pub fn counts(&self) -> (usize, usize) { - (self.insertions, self.deletions) - } - - pub fn is_empty(&self) -> bool { - self.diff.is_empty() - } -} - -#[derive(Debug, Error)] -#[error("utreexo error: {0}")] -#[repr(transparent)] -pub struct UtreexoError(String); - -#[derive(Debug, Default)] -#[repr(transparent)] -pub struct Accumulator(pub MemForest); - -impl Accumulator { - pub fn apply_diff( - &mut self, - diff: AccumulatorDiff, - ) -> Result<(), UtreexoError> { - let AccumulatorDiff { - diff, - insertions: n_insertions, - deletions: n_deletions, - } = diff; - let (mut insertions, mut deletions) = ( - Vec::with_capacity(n_insertions), - Vec::with_capacity(n_deletions), - ); - for (utxo_hash, insert) in diff { - if insert { - insertions.push(utxo_hash); - } else { - deletions.push(utxo_hash); - } - } - tracing::trace!( - leaves = %self.0.leaves, - roots = ?self.get_roots(), - insertions = ?insertions, - deletions = ?deletions, - "Applying diff" - ); - let () = self - .0 - .modify(&insertions, &deletions) - .map_err(UtreexoError)?; - tracing::debug!( - leaves = %self.0.leaves, - roots = ?self.get_roots(), - "Applied diff" - ); - Ok(()) - } +pub use thunder_types::*; - pub fn get_roots(&self) -> Vec { - self.0 - .get_roots() - .iter() - .map(|node| node.get_data()) - .collect() - } - - pub fn prove( - &self, - targets: &[BitcoinNodeHash], - ) -> Result, UtreexoError> { - self.0.prove(targets).map_err(UtreexoError) - } - - pub fn verify( - &self, - proof: &Proof, - del_hashes: &[BitcoinNodeHash], - ) -> 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)?; - let mem_forest = MemForest::deserialize(&*bytes) - .inspect_err(|err| { - tracing::debug!("deserialize err: {err}\n bytes: {bytes:?}") - }) - .map_err(::custom)?; - Ok(Self(mem_forest)) - } -} - -impl Serialize for Accumulator { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut bytes = Vec::new(); - self.0 - .serialize(&mut bytes) - .map_err(::custom)?; - as Serialize>::serialize(&bytes, serializer) - } -} - -/// Hash to get a [`CmbtNode`] inner commitment for a leaf value -#[derive(Debug)] -struct CbmtLeafPreCommitment<'a> { - fee: bitcoin::Amount, - /// Sum of canonical tx sizes for child txs - canonical_size: u64, - tx: &'a Transaction, -} - -impl<'a> BorshSerialize for CbmtLeafPreCommitment<'a> { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let Self { - fee, - canonical_size, - tx, - } = self; - BorshSerialize::serialize(&fee.to_sat(), writer)?; - BorshSerialize::serialize(canonical_size, writer)?; - BorshSerialize::serialize(tx, writer) - } -} - -/// Hash to get a [`CmbtNode`] inner commitment for a non-leaf value -#[derive(Debug)] -struct CbmtNodePreCommitment { - /// left child inner commitment - left_commitment: Hash, - /// Sum of child tx fees - fees: bitcoin::Amount, - /// Sum of canonical sizes of child txs - canonical_size: u64, - /// right child inner commitment - right_commitment: Hash, -} - -impl BorshSerialize for CbmtNodePreCommitment { - fn serialize( - &self, - writer: &mut W, - ) -> std::io::Result<()> { - let Self { - left_commitment, - fees, - canonical_size, - right_commitment, - } = self; - BorshSerialize::serialize(left_commitment, writer)?; - BorshSerialize::serialize(&fees.to_sat(), writer)?; - BorshSerialize::serialize(canonical_size, writer)?; - BorshSerialize::serialize(right_commitment, writer) - } -} - -// Internal node of a CBMT -#[derive(Clone, Debug, Default, Eq, PartialEq)] -struct CbmtNode { - // Commitment to child nodes or leaf value - commitment: Hash, - // Sum of fees for child nodes or leaf value - fees: bitcoin::Amount, - // Sum of canonical tx sizes for child nodes or leaf value - canonical_size: u64, - // CBT index, see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct - // This is required so that `CbmtNode` can be `Ord` correctly - index: usize, -} - -impl PartialOrd for CbmtNode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for CbmtNode { - fn cmp(&self, other: &Self) -> Ordering { - self.index.cmp(&other.index) - } -} - -// Marker type for merging branch commitments with -// * branch fee totals -// * branch canonical size totals -struct MergeFeeSizeTotal; - -impl merkle_cbt::merkle_tree::Merge for MergeFeeSizeTotal { - type Item = CbmtNode; - - fn merge(lnode: &Self::Item, rnode: &Self::Item) -> Self::Item { - let fees = lnode.fees + rnode.fees; - let canonical_size = lnode.canonical_size + rnode.canonical_size; - // see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct - assert_eq!(lnode.index + 1, rnode.index); - let index = (lnode.index - 1) / 2; - let commitment = hashes::hash(&CbmtNodePreCommitment { - left_commitment: lnode.commitment, - fees, - canonical_size, - right_commitment: rnode.commitment, - }); - Self::Item { - commitment, - fees, - canonical_size, - index, - } - } -} - -// Complete binary merkle tree with annotated fee and canonical size totals -type CbmtWithFeeTotal = merkle_cbt::CBMT; - -#[derive(Debug, Error)] -#[error("failed to compute fee for `{txid}`")] -struct ComputeMerkleRootErrorInner { - txid: Txid, - source: ComputeFeeError, -} - -#[derive(Debug, Error)] -#[error("failed to compute merkle root")] -#[repr(transparent)] -pub struct ComputeMerkleRootError(#[from] ComputeMerkleRootErrorInner); - -#[derive(Debug, Error)] -pub enum ModifyMemForestError { - #[error(transparent)] - ComputeMerkleRoot(#[from] ComputeMerkleRootError), - #[error(transparent)] - Utreexo(#[from] UtreexoError), -} - -#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema)] -pub struct Body { - pub coinbase: Vec, - pub transactions: Vec, - pub authorizations: Vec, -} - -impl Body { - pub fn new( - authorized_transactions: Vec, - coinbase: Vec, - ) -> Self { - let mut authorizations = Vec::with_capacity( - authorized_transactions - .iter() - .map(|t| t.transaction.inputs.len()) - .sum(), - ); - let mut transactions = - Vec::with_capacity(authorized_transactions.len()); - for at in authorized_transactions.into_iter() { - authorizations.extend(at.authorizations); - transactions.push(at.transaction); - } - Self { - coinbase, - transactions, - authorizations, - } - } - - pub fn authorized_transactions(&self) -> Vec { - let mut authorizations_iter = self.authorizations.iter(); - self.transactions - .iter() - .map(|tx| { - let mut authorizations = Vec::with_capacity(tx.inputs.len()); - for _ in 0..tx.inputs.len() { - let auth = authorizations_iter.next().unwrap(); - authorizations.push(auth.clone()); - } - AuthorizedTransaction { - transaction: tx.clone(), - authorizations, - } - }) - .collect() - } - - pub fn compute_merkle_root( - coinbase: &[Output], - txs: &[FilledTx], - ) -> Result - where - FilledTx: Borrow, - { - let CbmtNode { - commitment: txs_root, - .. - } = { - let n_txs = txs.len(); - let leaves: Vec<_> = txs - .iter() - .enumerate() - .map(|(idx, tx)| { - let tx = tx.borrow(); - let fees = tx.get_fee().map_err(|err| { - ComputeMerkleRootErrorInner { - txid: tx.transaction.txid(), - source: err, - } - })?; - let canonical_size = tx.transaction.canonical_size(); - let leaf_pre_commitment = CbmtLeafPreCommitment { - fee: fees, - canonical_size, - tx: &tx.transaction, - }; - Ok::<_, ComputeMerkleRootError>(CbmtNode { - commitment: hashes::hash(&leaf_pre_commitment), - fees, - canonical_size, - // see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct - index: (idx + n_txs) - 1, - }) - }) - .collect::>()?; - CbmtWithFeeTotal::build_merkle_root(leaves.as_slice()) - }; - // FIXME: Compute actual merkle root instead of just a hash. - let coinbase_root = hashes::hash(&coinbase); - // TODO: Should this include `total_fees`? - let root = hash_with_scratch_buffer(&(coinbase_root, txs_root)).into(); - Ok(root) - } - - // Modifies the memforest, without checking tx proofs - pub fn modify_memforest( - coinbase: &[Output], - txs: &[FilledTx], - memforest: &mut MemForest, - ) -> Result - where - FilledTx: Borrow, - { - // New leaves for the accumulator - let mut accumulator_add = Vec::::new(); - // Accumulator leaves to delete - let mut accumulator_del = Vec::::new(); - let merkle_root = Self::compute_merkle_root(coinbase, txs)?; - for (vout, output) in coinbase.iter().enumerate() { - let outpoint = OutPoint::Coinbase { - merkle_root, - vout: vout as u32, - }; - let pointed_output = PointedOutput { - outpoint, - output: output.clone(), - }; - accumulator_add.push((&pointed_output).into()); - } - for tx in txs { - let tx = tx.borrow(); - let txid = tx.transaction.txid(); - for (_, utxo_hash) in tx.transaction.inputs.iter() { - accumulator_del.push(utxo_hash.into()); - } - for (vout, output) in tx.transaction.outputs.iter().enumerate() { - let outpoint = OutPoint::Regular { - txid, - vout: vout as u32, - }; - let pointed_output = PointedOutput { - outpoint, - output: output.clone(), - }; - accumulator_add.push((&pointed_output).into()); - } - } - let () = memforest - .modify(&accumulator_add, &accumulator_del) - .map_err(UtreexoError)?; - Ok(merkle_root) - } - - pub fn get_inputs(&self) -> Vec { - self.transactions - .iter() - .flat_map(|tx| tx.inputs.iter().map(|(outpoint, _)| outpoint)) - .copied() - .collect() - } - - pub fn get_outputs( - coinbase: &[Output], - txs: &[FilledTransaction], - ) -> Result, ComputeMerkleRootError> { - let mut res = HashMap::new(); - let merkle_root = Self::compute_merkle_root(coinbase, txs)?; - for (vout, output) in coinbase.iter().enumerate() { - let vout = vout as u32; - let outpoint = OutPoint::Coinbase { merkle_root, vout }; - res.insert(outpoint, output.clone()); - } - for tx in txs { - let txid = tx.transaction.txid(); - for (vout, output) in tx.transaction.outputs.iter().enumerate() { - let vout = vout as u32; - let outpoint = OutPoint::Regular { txid, vout }; - res.insert(outpoint, output.clone()); - } - } - Ok(res) - } - - pub fn get_coinbase_value( - &self, - ) -> Result { - use bitcoin::amount::CheckedSum as _; - self.coinbase - .iter() - .map(|output| output.get_value()) - .checked_sum() - .ok_or(AmountOverflowError) - } - - /// Calculate total number of inputs across all transactions in a block body - pub fn inputs_len(&self) -> usize { - self.transactions.iter().map(|t| t.inputs.len()).sum() - } -} - -pub trait Verify { - type Error; - fn verify_transaction( - transaction: &AuthorizedTransaction, - ) -> Result<(), Self::Error>; - fn verify_body(body: &Body) -> Result<(), Self::Error>; -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub enum BmmResult { - Verified, - Failed, -} - -/// A tip refers to both a sidechain block AND the mainchain block that commits -/// to it. -#[derive( - BorshSerialize, - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - PartialEq, - Serialize, -)] -pub struct Tip { - pub block_hash: BlockHash, - #[borsh(serialize_with = "borsh_serialize_bitcoin_block_hash")] - pub main_block_hash: bitcoin::BlockHash, -} - -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr( - feature = "clap", - derive(clap::ValueEnum, strum::Display), - strum(serialize_all = "lowercase") -)] -pub enum Network { - #[default] - Signet, - Regtest, - Forknet, -} - -/// Semver-compatible version -#[derive( - BorshSerialize, - Clone, - Copy, - Debug, - Deserialize, - Eq, - Hash, - Ord, - PartialEq, - PartialOrd, - Serialize, -)] -pub struct Version { - pub major: u64, - pub minor: u64, - pub patch: u64, -} - -impl std::fmt::Display for Version { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}.{}", self.major, self.minor, self.patch) - } -} - -impl From for Version { - fn from(version: semver::Version) -> Self { - let semver::Version { - major, - minor, - patch, - pre: _, - build: _, - } = version; - Self { - major, - minor, - patch, - } - } -} +pub mod proto; // Do not make this public outside of this crate, as it could break semver pub(crate) static VERSION: LazyLock = LazyLock::new(|| { const VERSION_STR: &str = env!("CARGO_PKG_VERSION"); semver::Version::parse(VERSION_STR).unwrap().into() }); - -#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] -pub struct Block { - pub header: Header, - pub body: Body, -} - -#[cfg(test)] -mod withdrawal_bundle_order_regression { - use super::*; - use std::collections::{BTreeMap, HashMap}; - - use bitcoin::{Address, Amount, address::NetworkUnchecked}; - - fn aw(value: u64, main_fee: u64) -> AggregatedWithdrawal { - // value/main_fee drive the comparison; one address is enough to expose it. - let addr: Address = - "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" - .parse() - .unwrap(); - AggregatedWithdrawal { - spend_utxos: HashMap::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. - fn bundle_m6id(mut aggregated: Vec) -> M6id { - aggregated.sort_by_key(|a| std::cmp::Reverse(a.clone())); - let outputs: Vec = aggregated - .iter() - .map(|a| bitcoin::TxOut { - value: a.value, - script_pubkey: a - .main_address - .assume_checked_ref() - .script_pubkey(), - }) - .collect(); - WithdrawalBundle::new(0, Amount::ZERO, BTreeMap::new(), outputs) - .unwrap() - .compute_m6id() - } - - // The withdrawal bundle's m6id must not depend on the order in which withdrawals - // were aggregated (HashMap iteration order is randomized per process). Before the - // total-order fix, the comparator was non-transitive and this failed. - #[test] - fn m6id_is_independent_of_aggregation_order() { - let a = aw(1, 3); - let b = aw(3, 2); - let c = aw(2, 1); - let m = bundle_m6id(vec![a.clone(), b.clone(), c.clone()]); - for perm in [ - vec![c.clone(), b.clone(), a.clone()], - vec![b.clone(), a.clone(), c.clone()], - vec![a.clone(), c.clone(), b.clone()], - ] { - assert_eq!( - m, - bundle_m6id(perm), - "m6id must not depend on aggregation order" - ); - } - } -} diff --git a/lib/wallet.rs b/lib/wallet.rs index 4ffd379e..fc2200cc 100644 --- a/lib/wallet.rs +++ b/lib/wallet.rs @@ -3,45 +3,27 @@ use std::{ path::Path, }; -use bitcoin::Amount; use byteorder::{BigEndian, ByteOrder}; use ed25519_dalek_bip32::{ChildIndex, DerivationPath, ExtendedSigningKey}; use fallible_iterator::FallibleIterator as _; use futures::{Stream, StreamExt}; use heed::types::{Bytes, SerdeBincode, U8}; -use rustreexo::accumulator::node_hash::BitcoinNodeHash; -use serde::{Deserialize, Serialize}; use sneed::{Env, EnvError, RwTxnError, UnitKey, db::error::Error as DbError}; use tokio_stream::{StreamMap, wrappers::WatchStream}; -pub use crate::{ - authorization::{Authorization, get_address}, - types::{ - Address, AuthorizedTransaction, GetValue, InPoint, OutPoint, - OutPointKey, Output, OutputContent, SpentOutput, Transaction, - }, -}; use crate::{ types::{ - Accumulator, AmountOverflowError, AmountUnderflowError, PointedOutput, - UtreexoError, VERSION, Version, hash, + Accumulator, Address, AmountOverflowError, AmountUnderflowError, + AuthorizedTransaction, GetValue, InPoint, OutPoint, OutPointKey, + Output, OutputContent, PointedOutput, SpentOutput, Transaction, + UtreexoError, UtreexoNodeHash, VERSION, Version, + authorization::{Authorization, get_address}, + hash, + wallet::Balance, }, util::Watchable, }; -#[derive(Clone, Debug, Default, Deserialize, Serialize, utoipa::ToSchema)] -pub struct Balance { - #[serde(rename = "total_sats", with = "bitcoin::amount::serde::as_sat")] - #[schema(value_type = u64)] - pub total: Amount, - #[serde( - rename = "available_sats", - with = "bitcoin::amount::serde::as_sat" - )] - #[schema(value_type = u64)] - pub available: Amount, -} - #[derive(Debug, thiserror::Error)] pub enum Error { #[error("address {address} does not exist")] @@ -51,7 +33,7 @@ pub enum Error { #[error(transparent)] AmountUnderflow(#[from] AmountUnderflowError), #[error("authorization error")] - Authorization(#[from] crate::authorization::Error), + Authorization(#[from] crate::types::error::Authorization), #[error("bip32 error")] Bip32(#[from] ed25519_dalek_bip32::Error), #[error(transparent)] @@ -262,7 +244,7 @@ impl Wallet { (outpoint, utxo_hash) }) .collect(); - let input_utxo_hashes: Vec = + let input_utxo_hashes: Vec = inputs.iter().map(|(_, hash)| hash.into()).collect(); let proof = accumulator.prove(&input_utxo_hashes)?; let outputs = vec![ @@ -303,7 +285,7 @@ impl Wallet { (outpoint, utxo_hash) }) .collect(); - let input_utxo_hashes: Vec = + let input_utxo_hashes: Vec = inputs.iter().map(|(_, hash)| hash.into()).collect(); let proof = accumulator.prove(&input_utxo_hashes)?; let outputs = vec![ @@ -479,7 +461,7 @@ impl Wallet { let index = BigEndian::read_u32(&index); let signing_key = self.get_signing_key(&txn, index)?; let signature = - crate::authorization::sign(&signing_key, &transaction)?; + crate::types::authorization::sign(&signing_key, &transaction)?; authorizations.push(Authorization { verifying_key: signing_key.verifying_key(), signature, diff --git a/rpc-api/Cargo.toml b/rpc-api/Cargo.toml index 28893492..340e5272 100644 --- a/rpc-api/Cargo.toml +++ b/rpc-api/Cargo.toml @@ -12,7 +12,7 @@ jsonrpsee = { workspace = true, features = ["client", "macros", "server"] } l2l-openapi = { workspace = true } serde = {workspace = true } serde_json = { workspace = true } -thunder = { path = "../lib" } +thunder_types = { path = "../types" } utoipa = { workspace = true } [lints] diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index d32deb81..eb261819 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -5,14 +5,10 @@ use std::net::SocketAddr; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use l2l_openapi::open_api; use serde::{Deserialize, Serialize}; -use thunder::{ - net::Peer, - types::{ - Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, - OutputContent, PointedOutput, Transaction, Txid, WithdrawalBundle, - schema as thunder_schema, - }, - wallet::Balance, +use thunder_types::{ + Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, + OutputContent, PointedOutput, Transaction, Txid, WithdrawalBundle, + net::Peer, schema as thunder_schema, wallet::Balance, }; use utoipa::ToSchema; @@ -74,7 +70,7 @@ pub trait Rpc { async fn create_withdrawal( &self, #[open_api_method_arg(schema( - PartialSchema = "thunder::types::schema::BitcoinAddr" + PartialSchema = "thunder_schema::BitcoinAddr" ))] mainchain_address: bitcoin::Address< bitcoin::address::NetworkUnchecked, @@ -110,8 +106,8 @@ pub trait Rpc { #[method(name = "get_block")] async fn get_block( &self, - block_hash: thunder::types::BlockHash, - ) -> RpcResult>; + block_hash: thunder_types::BlockHash, + ) -> RpcResult>; /// Get mainchain blocks that commit to a specified block hash #[open_api_method(output_schema( @@ -120,7 +116,7 @@ pub trait Rpc { #[method(name = "get_bmm_inclusions")] async fn get_bmm_inclusions( &self, - block_hash: thunder::types::BlockHash, + block_hash: thunder_types::BlockHash, ) -> RpcResult>; /// Get the best mainchain block hash known by Thunder @@ -134,12 +130,12 @@ pub trait Rpc { /// Get the best sidechain block hash known by Thunder #[open_api_method(output_schema( - PartialSchema = "schema::Optional" + PartialSchema = "schema::Optional" ))] #[method(name = "get_best_sidechain_block_hash")] async fn get_best_sidechain_block_hash( &self, - ) -> RpcResult>; + ) -> RpcResult>; /// Get a new address #[method(name = "get_new_address")] diff --git a/types/Cargo.toml b/types/Cargo.toml new file mode 100644 index 00000000..de799a18 --- /dev/null +++ b/types/Cargo.toml @@ -0,0 +1,45 @@ +[build-dependencies] + +[dependencies] +bitcoin = { workspace = true, features = ["serde"] } +blake3 = { workspace = true } +borsh = { workspace = true, features = ["derive"] } +clap = { workspace = true, features = ["derive"], optional = true } +ed25519-dalek = { workspace = true, features = ["batch", "serde"] } +hashlink = { workspace = true, features = ["serde_impl"] } +heed = { workspace = true, optional = true } +hex = { workspace = true, features = ["serde"] } +merkle-cbt = { workspace = true } +rayon = { workspace = true } +rustreexo = { workspace = true, features = ["with-serde"] } +semver = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } +smallvec = { workspace = true, features = ["write"] } +strum = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +utoipa = { workspace = true, features = ["macros", "non_strict_integers"] } + +[dev-dependencies] +anyhow = { workspace = true } + +[features] +clap = ["dep:clap"] +default = [] +heed = ["dep:heed"] + +[lib] +name = "thunder_types" +path = "lib.rs" + +[lints] +workspace = true + +[package] +name = "thunder_types" +authors.workspace = true +edition.workspace = true +license-file.workspace = true +publish.workspace = true +version.workspace = true diff --git a/lib/types/address.rs b/types/address.rs similarity index 86% rename from lib/types/address.rs rename to types/address.rs index 9e8b5158..e0d9be49 100644 --- a/lib/types/address.rs +++ b/types/address.rs @@ -4,15 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{DeserializeAs, DisplayFromStr}; use utoipa::ToSchema; -use crate::types::THIS_SIDECHAIN; - -#[derive(Debug, thiserror::Error)] -pub enum AddressParseError { - #[error("bs58 error")] - Bs58(#[from] bitcoin::base58::InvalidCharacterError), - #[error("wrong address length {0} != 20")] - WrongLength(usize), -} +use crate::{THIS_SIDECHAIN, error::ParseAddress as ParseAddressError}; #[derive( BorshDeserialize, BorshSerialize, Clone, Copy, Eq, Hash, PartialEq, ToSchema, @@ -55,11 +47,11 @@ impl From<[u8; 20]> for Address { } impl std::str::FromStr for Address { - type Err = AddressParseError; + type Err = ParseAddressError; fn from_str(s: &str) -> Result { let address = bitcoin::base58::decode(s)?; Ok(Address(address.try_into().map_err( - |address: Vec| AddressParseError::WrongLength(address.len()), + |address: Vec| ParseAddressError::WrongLength(address.len()), )?)) } } diff --git a/lib/authorization.rs b/types/authorization.rs similarity index 81% rename from lib/authorization.rs rename to types/authorization.rs index b7a702f6..0821caf8 100644 --- a/lib/authorization.rs +++ b/types/authorization.rs @@ -6,53 +6,16 @@ use rayon::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::types::{ +use crate::{ Address, AuthorizedTransaction, Body, GetAddress, Transaction, Verify, + error::Authorization as Error, util::borsh::serialize as borsh_serialize, }; -pub use ed25519_dalek::{ - Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey, -}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("borsh serialization error")] - BorshSerialize(#[from] borsh::io::Error), - #[error("ed25519_dalek error")] - Dalek(#[from] SignatureError), - #[error("not enough authorizations")] - NotEnoughAuthorizations, - #[error("too many authorizations")] - TooManyAuthorizations, - #[error( - "wrong key for address: address = {address}, - hash(verifying_key) = {hash_verifying_key}" - )] - WrongKeyForAddress { - address: Address, - hash_verifying_key: Address, - }, -} +pub use ed25519_dalek::{Signer, Verifier}; -fn borsh_serialize_verifying_key( - vk: &VerifyingKey, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - borsh::BorshSerialize::serialize(&vk.to_bytes(), writer) -} - -fn borsh_serialize_signature( - sig: &Signature, - writer: &mut W, -) -> borsh::io::Result<()> -where - W: borsh::io::Write, -{ - borsh::BorshSerialize::serialize(&sig.to_bytes(), writer) -} +pub type Signature = ed25519_dalek::Signature; +pub type SigningKey = ed25519_dalek::SigningKey; +pub type VerifyingKey = ed25519_dalek::VerifyingKey; #[derive( BorshSerialize, @@ -65,10 +28,10 @@ where ToSchema, )] pub struct Authorization { - #[borsh(serialize_with = "borsh_serialize_verifying_key")] + #[borsh(serialize_with = "borsh_serialize::verifying_key")] #[schema(value_type = String)] pub verifying_key: VerifyingKey, - #[borsh(serialize_with = "borsh_serialize_signature")] + #[borsh(serialize_with = "borsh_serialize::signature")] #[schema(value_type = String)] pub signature: Signature, } diff --git a/types/error.rs b/types/error.rs new file mode 100644 index 00000000..58ebce7e --- /dev/null +++ b/types/error.rs @@ -0,0 +1,52 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("Bitcoin amount overflow")] +pub struct AmountOverflow; + +#[derive(Debug, Error)] +#[error("Bitcoin amount underflow")] +pub struct AmountUnderflow; + +#[derive(Debug, Error)] +pub enum Authorization { + #[error("borsh serialization error")] + BorshSerialize(#[from] borsh::io::Error), + #[error("ed25519 error")] + Ed25519(#[from] ed25519_dalek::SignatureError), + #[error("not enough authorizations")] + NotEnoughAuthorizations, + #[error("too many authorizations")] + TooManyAuthorizations, + #[error( + "wrong key for address: address = {address}, + hash(verifying_key) = {hash_verifying_key}" + )] + WrongKeyForAddress { + address: crate::Address, + hash_verifying_key: crate::Address, + }, +} + +#[derive(Debug, Error)] +pub enum ComputeFee { + #[error("underfunded (value in < value out)")] + Underfunded, + #[error("value in overflow")] + ValueInOverflow(#[source] AmountOverflow), + #[error("value out overflow")] + ValueOutOverflow(#[source] AmountOverflow), +} + +#[derive(Debug, Error)] +pub enum ParseAddress { + #[error("bs58 error")] + Bs58(#[from] bitcoin::base58::InvalidCharacterError), + #[error("wrong address length {0} != 20")] + WrongLength(usize), +} + +#[derive(Debug, Error)] +#[error("utreexo error ({0})")] +#[repr(transparent)] +pub struct Utreexo(pub(crate) String); diff --git a/lib/types/hashes.rs b/types/hashes.rs similarity index 94% rename from lib/types/hashes.rs rename to types/hashes.rs index def25caa..c4a24006 100644 --- a/lib/types/hashes.rs +++ b/types/hashes.rs @@ -5,12 +5,14 @@ use borsh::{BorshDeserialize, BorshSerialize}; use hex::FromHex; use serde::{Deserialize, Serialize}; -use super::serde_hexstr_human_readable; +use crate::util::serde::hexstr_human_readable; const BLAKE3_LENGTH: usize = 32; pub type Hash = [u8; BLAKE3_LENGTH]; +pub type UtreexoNodeHash = rustreexo::accumulator::node_hash::BitcoinNodeHash; + #[derive( BorshSerialize, BorshDeserialize, @@ -24,7 +26,7 @@ pub type Hash = [u8; BLAKE3_LENGTH]; PartialOrd, Serialize, )] -pub struct BlockHash(#[serde(with = "serde_hexstr_human_readable")] pub Hash); +pub struct BlockHash(#[serde(with = "hexstr_human_readable")] pub Hash); impl From for BlockHash { fn from(other: Hash) -> Self { @@ -98,7 +100,7 @@ impl utoipa::ToSchema for BlockHash { PartialOrd, Serialize, )] -pub struct MerkleRoot(#[serde(with = "serde_hexstr_human_readable")] Hash); +pub struct MerkleRoot(#[serde(with = "hexstr_human_readable")] Hash); impl From for MerkleRoot { fn from(other: Hash) -> Self { @@ -154,7 +156,7 @@ impl utoipa::ToSchema for MerkleRoot { )] #[repr(transparent)] #[serde(transparent)] -pub struct Txid(#[serde(with = "serde_hexstr_human_readable")] pub Hash); +pub struct Txid(#[serde(with = "hexstr_human_readable")] pub Hash); impl Txid { pub fn as_slice(&self) -> &[u8] { diff --git a/types/lib.rs b/types/lib.rs new file mode 100644 index 00000000..dd238d00 --- /dev/null +++ b/types/lib.rs @@ -0,0 +1,1033 @@ +use std::{ + borrow::Borrow, + cmp::Ordering, + collections::{BTreeMap, HashMap}, + sync::LazyLock, +}; + +use borsh::BorshSerialize; +use hashlink::{LinkedHashMap, linked_hash_map}; +use rustreexo::accumulator::mem_forest::MemForest; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use thiserror::Error; +use utoipa::ToSchema; + +mod address; +pub use address::Address; +pub mod authorization; +pub use authorization::Authorization; +pub mod error; +pub use error::{ + AmountOverflow as AmountOverflowError, + AmountUnderflow as AmountUnderflowError, ComputeFee as ComputeFeeError, + Utreexo as UtreexoError, +}; +pub mod hashes; +pub use hashes::{ + BlockHash, Hash, M6id, MerkleRoot, Txid, UtreexoNodeHash, hash, + hash_with_scratch_buffer, +}; +pub mod net; +pub mod schema; +pub mod transaction; +pub use transaction::{ + Authorized, AuthorizedTransaction, Content as OutputContent, + FilledTransaction, GetAddress, GetValue, InPoint, OutPoint, OutPointKey, + Output, PointedOutput, PointedOutputRef, SpentOutput, Transaction, +}; +mod util; +pub mod wallet; + +pub const THIS_SIDECHAIN: u8 = 9; + +pub type UtreexoProof = rustreexo::accumulator::proof::Proof; + +#[derive( + BorshSerialize, + Clone, + Debug, + Deserialize, + Eq, + Hash, + PartialEq, + Serialize, + ToSchema, +)] +pub struct Header { + pub merkle_root: MerkleRoot, + pub prev_side_hash: Option, + #[borsh(serialize_with = "util::borsh::serialize::bitcoin_block_hash")] + #[schema(value_type = schema::BitcoinBlockHash)] + pub prev_main_hash: bitcoin::BlockHash, + /// Utreexo roots + #[borsh(serialize_with = "util::borsh::serialize::utreexo_roots")] + #[schema(value_type = Vec)] + pub roots: Vec, +} + +impl Header { + pub fn hash(&self) -> BlockHash { + hash_with_scratch_buffer(self).into() + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum WithdrawalBundleEventStatus { + Confirmed, + Failed, + Submitted, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum WithdrawalBundleStatus { + Confirmed, + /// Formerly pending bundle + Dropped, + Failed, + Pending, + Submitted, + /// Submitted, but unexpected due to previously being dropped or failing. + /// It may not be possible to account for this withdrawal bundle, if it + /// double-spends UTXOs. + SubmittedUnexpected, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct WithdrawalBundleEvent { + pub m6id: M6id, + pub status: WithdrawalBundleEventStatus, +} + +pub static OP_DRIVECHAIN_SCRIPT: LazyLock = + LazyLock::new(|| { + let mut script = bitcoin::ScriptBuf::new(); + script.push_opcode(bitcoin::opcodes::all::OP_RETURN); + script.push_instruction(bitcoin::script::Instruction::PushBytes( + &bitcoin::script::PushBytesBuf::from([THIS_SIDECHAIN]), + )); + script.push_opcode(bitcoin::opcodes::OP_TRUE); + script + }); + +#[derive(Debug, Error)] +enum WithdrawalBundleErrorInner { + #[error("bundle too heavy: weight `{weight}` > max weight `{max_weight}`")] + BundleTooHeavy { weight: u64, max_weight: u64 }, +} + +#[derive(Debug, Error)] +#[error("Withdrawal bundle error")] +pub struct WithdrawalBundleError(#[from] WithdrawalBundleErrorInner); + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] +pub struct WithdrawalBundle { + #[schema(value_type = Vec<(transaction::OutPoint, transaction::Output)>)] + #[serde_as(as = "serde_with::IfIsHumanReadable>")] + spend_utxos: BTreeMap, + #[schema(value_type = schema::BitcoinTransaction)] + tx: bitcoin::Transaction, +} + +impl WithdrawalBundle { + /// Compute the size of a single txout + pub const fn txout_size(spk_size: u32) -> Option { + let Some(size) = (bitcoin::Amount::SIZE as u32) + .checked_add(bitcoin::VarInt(spk_size as u64).size() as u32) + else { + return None; + }; + size.checked_add(spk_size) + } + + /// Predict the weight of a withdrawal bundle, based on the number of + /// outputs (not including the commitment/treasury outputs) and the + /// sum of sizes of txouts (not including the commitment/treasury outputs). + /// Returns None if the predicted weight exceeds the maximum tx weight. + pub const fn predict_weight( + n_outputs: u32, + sum_txout_sizes: u32, + ) -> Option { + use bitcoin::{VarInt, Weight}; + const fn txin_base_size(script_sig_size: u32) -> Option { + const OUTPOINT_SIZE: u8 = 36; + const SEQUENCE_SIZE: u8 = 4; + let script_sig_len_size: u8 = + VarInt(script_sig_size as u64).size() as u8; + let Some(res) = ((OUTPOINT_SIZE + script_sig_len_size) as u32) + .checked_add(script_sig_size) + else { + return None; + }; + res.checked_add(SEQUENCE_SIZE as u32) + } + const fn tx_base_size( + n_inputs: u32, + sum_txin_base_sizes: u32, + n_outputs: u32, + sum_txout_sizes: u32, + ) -> Option { + const VERSION_SIZE: u8 = 4; + const fn vin_base_size( + n_inputs: u32, + sum_txin_base_sizes: u32, + ) -> Option { + let len_size = VarInt(n_inputs as u64).size() as u8; + (len_size as u32).checked_add(sum_txin_base_sizes) + } + const fn vout_size( + n_outputs: u32, + sum_txout_sizes: u32, + ) -> Option { + let len_size = VarInt(n_outputs as u64).size() as u8; + (len_size as u32).checked_add(sum_txout_sizes) + } + const LOCKTIME_SIZE: u8 = bitcoin::absolute::LockTime::SIZE as u8; + let res = VERSION_SIZE as u32; + let Some(vin_base_size) = + vin_base_size(n_inputs, sum_txin_base_sizes) + else { + return None; + }; + let Some(res) = res.checked_add(vin_base_size) else { + return None; + }; + let Some(vout_size) = vout_size(n_outputs, sum_txout_sizes) else { + return None; + }; + let Some(res) = res.checked_add(vout_size) else { + return None; + }; + res.checked_add(LOCKTIME_SIZE as u32) + } + const N_INPUTS: u32 = 1; + const SUM_TXIN_BASE_SIZES: u32 = { + const TREASURY_TXIN_BASE_SIZE: u32 = { + const TREASURY_SCRIPT_SIG_SIZE: u32 = 0; + txin_base_size(TREASURY_SCRIPT_SIG_SIZE).unwrap() + }; + TREASURY_TXIN_BASE_SIZE + }; + let Some(n_outputs) = n_outputs.checked_add(2) else { + return None; + }; + let Some(sum_txout_sizes) = ({ + const INPUTS_COMMITMENT_TXOUT_SIZE: u32 = { + const INPUTS_COMMITMENT_OUTPUT_SPK_SIZE: u8 = 34; + WithdrawalBundle::txout_size( + INPUTS_COMMITMENT_OUTPUT_SPK_SIZE as u32, + ) + .unwrap() + }; + const MAINCHAIN_FEE_COMMITMENT_TXOUT_SIZE: u32 = { + const MAINCHAIN_FEE_COMMITMENT_OUTPUT_SPK_SIZE: u8 = 10; + WithdrawalBundle::txout_size( + MAINCHAIN_FEE_COMMITMENT_OUTPUT_SPK_SIZE as u32, + ) + .unwrap() + }; + (INPUTS_COMMITMENT_TXOUT_SIZE + MAINCHAIN_FEE_COMMITMENT_TXOUT_SIZE) + .checked_add(sum_txout_sizes) + }) else { + return None; + }; + let Some(tx_base_size) = tx_base_size( + N_INPUTS, + SUM_TXIN_BASE_SIZES, + n_outputs, + sum_txout_sizes, + ) else { + return None; + }; + let Some(tx_weight_wu) = + (tx_base_size as u64).checked_mul(Weight::WITNESS_SCALE_FACTOR) + else { + return None; + }; + if tx_weight_wu <= bitcoin::Transaction::MAX_STANDARD_WEIGHT.to_wu() { + Some(Weight::from_wu(tx_weight_wu)) + } else { + None + } + } + + pub fn new( + block_height: u32, + fee: bitcoin::Amount, + 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 = hash(&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, + } + }; + let mainchain_fee_txout = { + let script_pubkey = bitcoin::script::Builder::new() + .push_opcode(bitcoin::opcodes::all::OP_RETURN) + .push_slice(fee.to_sat().to_be_bytes()) + .into_script(); + bitcoin::TxOut { + value: bitcoin::Amount::ZERO, + script_pubkey, + } + }; + let outputs = Vec::from_iter( + [mainchain_fee_txout, inputs_commitment_txout] + .into_iter() + .chain(bundle_outputs), + ); + let tx = bitcoin::Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::blockdata::locktime::absolute::LockTime::ZERO, + input: Vec::new(), + output: outputs, + }; + if tx.weight().to_wu() > bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64 + { + Err(WithdrawalBundleErrorInner::BundleTooHeavy { + weight: tx.weight().to_wu(), + max_weight: bitcoin::policy::MAX_STANDARD_TX_WEIGHT as u64, + })?; + } + Ok(Self { spend_utxos, tx }) + } + + pub fn compute_m6id(&self) -> M6id { + M6id(self.tx.compute_txid()) + } + + pub fn spend_utxos( + &self, + ) -> &BTreeMap { + &self.spend_utxos + } + + pub fn tx(&self) -> &bitcoin::Transaction { + &self.tx + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct TwoWayPegData { + pub deposits: HashMap, + pub deposit_block_hash: Option, + pub bundle_statuses: HashMap, +} + +/* +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DisconnectData { + pub spent_utxos: HashMap, + pub deposits: Vec, + pub pending_bundles: Vec, + pub spent_bundles: HashMap>, + pub spent_withdrawals: HashMap, + pub failed_withdrawals: Vec, +} +*/ + +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct AggregatedWithdrawal { + pub spend_utxos: HashMap, + pub main_address: bitcoin::Address, + pub value: bitcoin::Amount, + pub main_fee: bitcoin::Amount, +} + +impl Ord for AggregatedWithdrawal { + fn cmp(&self, other: &Self) -> Ordering { + // A *total* order (lexicographic by main_fee, value, main_address). The + // previous `OR of >` was not antisymmetric/transitive, so the + // withdrawal-bundle output order (and hence compute_m6id) depended on + // HashMap iteration order and could differ across nodes. A real total order makes + // the sorted bundle canonical regardless of aggregation order. + (self.main_fee, self.value, &self.main_address).cmp(&( + other.main_fee, + other.value, + &other.main_address, + )) + } +} + +impl PartialOrd for AggregatedWithdrawal { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// 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)] +pub struct AccumulatorDiff { + /// `true` indicates insertion, `false` indicates removal. + diff: LinkedHashMap, + /// Total number of insertions still represented in `diff`. + insertions: usize, + /// Total number of deletions still represented in `diff`. + deletions: usize, +} + +impl Default for AccumulatorDiff { + fn default() -> Self { + Self { + diff: LinkedHashMap::new(), + insertions: 0, + deletions: 0, + } + } +} + +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: UtreexoNodeHash) { + match self.diff.entry(utxo_hash) { + linked_hash_map::Entry::Occupied(entry) => { + if !entry.get() { + entry.remove(); + debug_assert!(self.deletions > 0); + self.deletions -= 1; + } + } + linked_hash_map::Entry::Vacant(entry) => { + entry.insert(true); + self.insertions += 1; + } + } + } + + pub fn remove(&mut self, utxo_hash: UtreexoNodeHash) { + match self.diff.entry(utxo_hash) { + linked_hash_map::Entry::Occupied(entry) => { + if *entry.get() { + entry.remove(); + debug_assert!(self.insertions > 0); + self.insertions -= 1; + } + } + linked_hash_map::Entry::Vacant(entry) => { + entry.insert(false); + self.deletions += 1; + } + } + } + + /// Returns the number of tracked insertions and deletions, in that order. + pub fn counts(&self) -> (usize, usize) { + (self.insertions, self.deletions) + } + + pub fn is_empty(&self) -> bool { + self.diff.is_empty() + } +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct Accumulator(pub MemForest); + +impl Accumulator { + pub fn apply_diff( + &mut self, + diff: AccumulatorDiff, + ) -> Result<(), UtreexoError> { + let AccumulatorDiff { + diff, + insertions: n_insertions, + deletions: n_deletions, + } = diff; + let (mut insertions, mut deletions) = ( + Vec::with_capacity(n_insertions), + Vec::with_capacity(n_deletions), + ); + for (utxo_hash, insert) in diff { + if insert { + insertions.push(utxo_hash); + } else { + deletions.push(utxo_hash); + } + } + tracing::trace!( + leaves = %self.0.leaves, + roots = ?self.get_roots(), + insertions = ?insertions, + deletions = ?deletions, + "Applying diff" + ); + let () = self + .0 + .modify(&insertions, &deletions) + .map_err(UtreexoError)?; + tracing::debug!( + leaves = %self.0.leaves, + roots = ?self.get_roots(), + "Applied diff" + ); + Ok(()) + } + + pub fn get_roots(&self) -> Vec { + self.0 + .get_roots() + .iter() + .map(|node| node.get_data()) + .collect() + } + + pub fn prove( + &self, + targets: &[UtreexoNodeHash], + ) -> Result { + self.0.prove(targets).map_err(UtreexoError) + } + + pub fn verify( + &self, + proof: &UtreexoProof, + del_hashes: &[UtreexoNodeHash], + ) -> 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)?; + let mem_forest = MemForest::deserialize(&*bytes) + .inspect_err(|err| { + tracing::debug!("deserialize err: {err}\n bytes: {bytes:?}") + }) + .map_err(::custom)?; + Ok(Self(mem_forest)) + } +} + +impl Serialize for Accumulator { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut bytes = Vec::new(); + self.0 + .serialize(&mut bytes) + .map_err(::custom)?; + as Serialize>::serialize(&bytes, serializer) + } +} + +/// Hash to get a [`CmbtNode`] inner commitment for a leaf value +#[derive(Debug)] +struct CbmtLeafPreCommitment<'a> { + fee: bitcoin::Amount, + /// Sum of canonical tx sizes for child txs + canonical_size: u64, + tx: &'a Transaction, +} + +impl<'a> BorshSerialize for CbmtLeafPreCommitment<'a> { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + let Self { + fee, + canonical_size, + tx, + } = self; + BorshSerialize::serialize(&fee.to_sat(), writer)?; + BorshSerialize::serialize(canonical_size, writer)?; + BorshSerialize::serialize(tx, writer) + } +} + +/// Hash to get a [`CmbtNode`] inner commitment for a non-leaf value +#[derive(Debug)] +struct CbmtNodePreCommitment { + /// left child inner commitment + left_commitment: Hash, + /// Sum of child tx fees + fees: bitcoin::Amount, + /// Sum of canonical sizes of child txs + canonical_size: u64, + /// right child inner commitment + right_commitment: Hash, +} + +impl BorshSerialize for CbmtNodePreCommitment { + fn serialize( + &self, + writer: &mut W, + ) -> std::io::Result<()> { + let Self { + left_commitment, + fees, + canonical_size, + right_commitment, + } = self; + BorshSerialize::serialize(left_commitment, writer)?; + BorshSerialize::serialize(&fees.to_sat(), writer)?; + BorshSerialize::serialize(canonical_size, writer)?; + BorshSerialize::serialize(right_commitment, writer) + } +} + +// Internal node of a CBMT +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct CbmtNode { + // Commitment to child nodes or leaf value + commitment: Hash, + // Sum of fees for child nodes or leaf value + fees: bitcoin::Amount, + // Sum of canonical tx sizes for child nodes or leaf value + canonical_size: u64, + // CBT index, see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct + // This is required so that `CbmtNode` can be `Ord` correctly + index: usize, +} + +impl PartialOrd for CbmtNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CbmtNode { + fn cmp(&self, other: &Self) -> Ordering { + self.index.cmp(&other.index) + } +} + +// Marker type for merging branch commitments with +// * branch fee totals +// * branch canonical size totals +struct MergeFeeSizeTotal; + +impl merkle_cbt::merkle_tree::Merge for MergeFeeSizeTotal { + type Item = CbmtNode; + + fn merge(lnode: &Self::Item, rnode: &Self::Item) -> Self::Item { + let fees = lnode.fees + rnode.fees; + let canonical_size = lnode.canonical_size + rnode.canonical_size; + // see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct + assert_eq!(lnode.index + 1, rnode.index); + let index = (lnode.index - 1) / 2; + let commitment = hashes::hash(&CbmtNodePreCommitment { + left_commitment: lnode.commitment, + fees, + canonical_size, + right_commitment: rnode.commitment, + }); + Self::Item { + commitment, + fees, + canonical_size, + index, + } + } +} + +// Complete binary merkle tree with annotated fee and canonical size totals +type CbmtWithFeeTotal = merkle_cbt::CBMT; + +#[derive(Debug, Error)] +#[error("failed to compute fee for `{txid}`")] +struct ComputeMerkleRootErrorInner { + txid: Txid, + source: ComputeFeeError, +} + +#[derive(Debug, Error)] +#[error("failed to compute merkle root")] +#[repr(transparent)] +pub struct ComputeMerkleRootError(#[from] ComputeMerkleRootErrorInner); + +#[derive(Debug, Error)] +pub enum ModifyMemForestError { + #[error(transparent)] + ComputeMerkleRoot(#[from] ComputeMerkleRootError), + #[error(transparent)] + Utreexo(#[from] UtreexoError), +} + +#[derive(BorshSerialize, Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct Body { + pub coinbase: Vec, + pub transactions: Vec, + pub authorizations: Vec, +} + +impl Body { + pub fn new( + authorized_transactions: Vec, + coinbase: Vec, + ) -> Self { + let mut authorizations = Vec::with_capacity( + authorized_transactions + .iter() + .map(|t| t.transaction.inputs.len()) + .sum(), + ); + let mut transactions = + Vec::with_capacity(authorized_transactions.len()); + for at in authorized_transactions.into_iter() { + authorizations.extend(at.authorizations); + transactions.push(at.transaction); + } + Self { + coinbase, + transactions, + authorizations, + } + } + + pub fn authorized_transactions(&self) -> Vec { + let mut authorizations_iter = self.authorizations.iter(); + self.transactions + .iter() + .map(|tx| { + let mut authorizations = Vec::with_capacity(tx.inputs.len()); + for _ in 0..tx.inputs.len() { + let auth = authorizations_iter.next().unwrap(); + authorizations.push(auth.clone()); + } + AuthorizedTransaction { + transaction: tx.clone(), + authorizations, + } + }) + .collect() + } + + pub fn compute_merkle_root( + coinbase: &[Output], + txs: &[FilledTx], + ) -> Result + where + FilledTx: Borrow, + { + let CbmtNode { + commitment: txs_root, + .. + } = { + let n_txs = txs.len(); + let leaves: Vec<_> = txs + .iter() + .enumerate() + .map(|(idx, tx)| { + let tx = tx.borrow(); + let fees = tx.get_fee().map_err(|err| { + ComputeMerkleRootErrorInner { + txid: tx.transaction.txid(), + source: err, + } + })?; + let canonical_size = tx.transaction.canonical_size(); + let leaf_pre_commitment = CbmtLeafPreCommitment { + fee: fees, + canonical_size, + tx: &tx.transaction, + }; + Ok::<_, ComputeMerkleRootError>(CbmtNode { + commitment: hashes::hash(&leaf_pre_commitment), + fees, + canonical_size, + // see https://github.com/nervosnetwork/merkle-tree/blob/5d1898263e7167560fdaa62f09e8d52991a1c712/README.md#tree-struct + index: (idx + n_txs) - 1, + }) + }) + .collect::>()?; + CbmtWithFeeTotal::build_merkle_root(leaves.as_slice()) + }; + // FIXME: Compute actual merkle root instead of just a hash. + let coinbase_root = hashes::hash(&coinbase); + // TODO: Should this include `total_fees`? + let root = hash_with_scratch_buffer(&(coinbase_root, txs_root)).into(); + Ok(root) + } + + // Modifies the memforest, without checking tx proofs + pub fn modify_memforest( + coinbase: &[Output], + txs: &[FilledTx], + memforest: &mut MemForest, + ) -> Result + where + FilledTx: Borrow, + { + // New leaves for the accumulator + let mut accumulator_add = Vec::::new(); + // Accumulator leaves to delete + let mut accumulator_del = Vec::::new(); + let merkle_root = Self::compute_merkle_root(coinbase, txs)?; + for (vout, output) in coinbase.iter().enumerate() { + let outpoint = OutPoint::Coinbase { + merkle_root, + vout: vout as u32, + }; + let pointed_output = PointedOutput { + outpoint, + output: output.clone(), + }; + accumulator_add.push((&pointed_output).into()); + } + for tx in txs { + let tx = tx.borrow(); + let txid = tx.transaction.txid(); + for (_, utxo_hash) in tx.transaction.inputs.iter() { + accumulator_del.push(utxo_hash.into()); + } + for (vout, output) in tx.transaction.outputs.iter().enumerate() { + let outpoint = OutPoint::Regular { + txid, + vout: vout as u32, + }; + let pointed_output = PointedOutput { + outpoint, + output: output.clone(), + }; + accumulator_add.push((&pointed_output).into()); + } + } + let () = memforest + .modify(&accumulator_add, &accumulator_del) + .map_err(UtreexoError)?; + Ok(merkle_root) + } + + pub fn get_inputs(&self) -> Vec { + self.transactions + .iter() + .flat_map(|tx| tx.inputs.iter().map(|(outpoint, _)| outpoint)) + .copied() + .collect() + } + + pub fn get_outputs( + coinbase: &[Output], + txs: &[FilledTransaction], + ) -> Result, ComputeMerkleRootError> { + let mut res = HashMap::new(); + let merkle_root = Self::compute_merkle_root(coinbase, txs)?; + for (vout, output) in coinbase.iter().enumerate() { + let vout = vout as u32; + let outpoint = OutPoint::Coinbase { merkle_root, vout }; + res.insert(outpoint, output.clone()); + } + for tx in txs { + let txid = tx.transaction.txid(); + for (vout, output) in tx.transaction.outputs.iter().enumerate() { + let vout = vout as u32; + let outpoint = OutPoint::Regular { txid, vout }; + res.insert(outpoint, output.clone()); + } + } + Ok(res) + } + + pub fn get_coinbase_value( + &self, + ) -> Result { + use bitcoin::amount::CheckedSum as _; + self.coinbase + .iter() + .map(|output| output.get_value()) + .checked_sum() + .ok_or(AmountOverflowError) + } + + /// Calculate total number of inputs across all transactions in a block body + pub fn inputs_len(&self) -> usize { + self.transactions.iter().map(|t| t.inputs.len()).sum() + } +} + +pub trait Verify { + type Error; + fn verify_transaction( + transaction: &AuthorizedTransaction, + ) -> Result<(), Self::Error>; + fn verify_body(body: &Body) -> Result<(), Self::Error>; +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum BmmResult { + Verified, + Failed, +} + +/// A tip refers to both a sidechain block AND the mainchain block that commits +/// to it. +#[derive( + BorshSerialize, + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + PartialEq, + Serialize, +)] +pub struct Tip { + pub block_hash: BlockHash, + #[borsh(serialize_with = "util::borsh::serialize::bitcoin_block_hash")] + pub main_block_hash: bitcoin::BlockHash, +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr( + feature = "clap", + derive(clap::ValueEnum, strum::Display), + strum(serialize_all = "lowercase") +)] +pub enum Network { + #[default] + Signet, + Regtest, + Forknet, +} + +/// Semver-compatible version +#[derive( + BorshSerialize, + Clone, + Copy, + Debug, + Deserialize, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + Serialize, +)] +pub struct Version { + pub major: u64, + pub minor: u64, + pub patch: u64, +} + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl From for Version { + fn from(version: semver::Version) -> Self { + let semver::Version { + major, + minor, + patch, + pre: _, + build: _, + } = version; + Self { + major, + minor, + patch, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct Block { + pub header: Header, + pub body: Body, +} + +#[cfg(test)] +mod withdrawal_bundle_order_regression { + use super::*; + use std::collections::{BTreeMap, HashMap}; + + use bitcoin::{Address, Amount, address::NetworkUnchecked}; + + fn aw(value: u64, main_fee: u64) -> AggregatedWithdrawal { + // value/main_fee drive the comparison; one address is enough to expose it. + let addr: Address = + "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + .parse() + .unwrap(); + AggregatedWithdrawal { + spend_utxos: HashMap::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. + fn bundle_m6id(mut aggregated: Vec) -> M6id { + aggregated.sort_by_key(|a| std::cmp::Reverse(a.clone())); + let outputs: Vec = aggregated + .iter() + .map(|a| bitcoin::TxOut { + value: a.value, + script_pubkey: a + .main_address + .assume_checked_ref() + .script_pubkey(), + }) + .collect(); + WithdrawalBundle::new(0, Amount::ZERO, BTreeMap::new(), outputs) + .unwrap() + .compute_m6id() + } + + // The withdrawal bundle's m6id must not depend on the order in which withdrawals + // were aggregated (HashMap iteration order is randomized per process). Before the + // total-order fix, the comparator was non-transitive and this failed. + #[test] + fn m6id_is_independent_of_aggregation_order() { + let a = aw(1, 3); + let b = aw(3, 2); + let c = aw(2, 1); + let m = bundle_m6id(vec![a.clone(), b.clone(), c.clone()]); + for perm in [ + vec![c.clone(), b.clone(), a.clone()], + vec![b.clone(), a.clone(), c.clone()], + vec![a.clone(), c.clone(), b.clone()], + ] { + assert_eq!( + m, + bundle_m6id(perm), + "m6id must not depend on aggregation order" + ); + } + } +} diff --git a/types/net.rs b/types/net.rs new file mode 100644 index 00000000..d6531a9d --- /dev/null +++ b/types/net.rs @@ -0,0 +1,29 @@ +pub mod peer { + use std::net::SocketAddr; + + use serde::{Deserialize, Serialize}; + use strum::Display; + use utoipa::ToSchema; + + use crate::schema; + + #[derive( + Clone, Copy, Deserialize, Display, Eq, PartialEq, Serialize, ToSchema, + )] + #[schema(as = PeerConnectionStatus)] + pub enum ConnectionStatus { + /// We're still in the process of initializing the peer connection + Connecting, + /// The connection is successfully established + Connected, + } + + /// RPC output representation for peer + state + #[derive(Clone, Deserialize, Serialize, ToSchema)] + pub struct Peer { + #[schema(value_type = schema::SocketAddr)] + pub address: SocketAddr, + pub status: ConnectionStatus, + } +} +pub use peer::{ConnectionStatus as PeerConnectionStatus, Peer}; diff --git a/lib/types/schema.rs b/types/schema.rs similarity index 100% rename from lib/types/schema.rs rename to types/schema.rs diff --git a/lib/types/transaction.rs b/types/transaction.rs similarity index 93% rename from lib/types/transaction.rs rename to types/transaction.rs index 25ccc1c8..6d333c71 100644 --- a/lib/types/transaction.rs +++ b/types/transaction.rs @@ -2,17 +2,16 @@ use std::{borrow::Borrow, io::Cursor}; use bitcoin::amount::CheckedSum; use borsh::{self, BorshDeserialize, BorshSerialize}; +#[cfg(feature = "heed")] use heed::{BoxedError, BytesDecode, BytesEncode}; -use rustreexo::accumulator::{node_hash::BitcoinNodeHash, proof::Proof}; +use rustreexo::accumulator::proof::Proof as UtreexoProof; use serde::{Deserialize, Serialize}; -use thiserror::Error; use utoipa::ToSchema; -use super::{ - Address, AmountOverflowError, Hash, M6id, MerkleRoot, Txid, hash, - hash_with_scratch_buffer, +use crate::{ + Address, AmountOverflowError, Authorization, ComputeFeeError, Hash, M6id, + MerkleRoot, Txid, UtreexoNodeHash, hash, hash_with_scratch_buffer, schema, }; -use crate::authorization::Authorization; pub trait GetAddress { fn get_address(&self) -> Address; @@ -82,7 +81,7 @@ pub enum OutPoint { vout: u32, }, // Created by mainchain deposits. - #[schema(value_type = crate::types::schema::BitcoinOutPoint)] + #[schema(value_type = schema::BitcoinOutPoint)] Deposit( #[borsh( deserialize_with = "borsh_deserialize_bitcoin_outpoint", @@ -176,7 +175,7 @@ impl AsRef<[u8]> for OutPointKey { } } -// Database key encoding traits for direct LMDB usage +#[cfg(feature = "heed")] impl<'a> BytesEncode<'a> for OutPointKey { type EItem = OutPointKey; @@ -188,6 +187,7 @@ impl<'a> BytesEncode<'a> for OutPointKey { } } +#[cfg(feature = "heed")] impl<'a> BytesDecode<'a> for OutPointKey { type DItem = OutPointKey; @@ -206,9 +206,10 @@ impl<'a> BytesDecode<'a> for OutPointKey { } #[cfg(test)] -mod tests { - use super::{OUTPOINT_KEY_SIZE, OutPoint, OutPointKey}; - use bitcoin::hashes::Hash as BitcoinHash; +mod test { + use bitcoin::hashes::Hash as _; + + use crate::transaction::{OUTPOINT_KEY_SIZE, OutPoint, OutPointKey}; #[test] fn check_outpoint_key_size() -> anyhow::Result<()> { @@ -246,10 +247,12 @@ mod tests { // fee, since both leave the treasury #[test] fn withdrawal_value_includes_main_fee() { - use super::{ - Content, FilledTransaction, GetValue, Output, Transaction, + use crate::{ + Address, + transaction::{ + Content, FilledTransaction, GetValue, Output, Transaction, + }, }; - use crate::types::Address; let value = bitcoin::Amount::from_sat(1000); let main_fee = bitcoin::Amount::from_sat(300); @@ -332,6 +335,8 @@ mod content { use serde::{Deserialize, Serialize}; use utoipa::{PartialSchema, ToSchema}; + use crate::{GetValue, schema}; + /// Default representation for Serde #[derive(Deserialize, Serialize)] enum DefaultRepr { @@ -360,7 +365,7 @@ mod content { #[serde(rename = "main_fee_sats")] #[schema(value_type = u64)] main_fee: bitcoin::Amount, - #[schema(value_type = crate::types::schema::BitcoinAddr)] + #[schema(value_type = schema::BitcoinAddr)] main_address: bitcoin::Address, }, } @@ -394,12 +399,12 @@ mod content { matches!(self, Self::Withdrawal { .. }) } - pub(in crate::types) fn schema_ref() -> utoipa::openapi::Ref { + pub(crate) fn schema_ref() -> utoipa::openapi::Ref { utoipa::openapi::Ref::new("OutputContent") } } - impl crate::types::GetValue for Content { + impl GetValue for Content { #[inline(always)] fn get_value(&self) -> bitcoin::Amount { match self { @@ -558,7 +563,7 @@ pub struct PointedOutput { pub output: Output, } -impl From<&PointedOutput> for BitcoinNodeHash { +impl From<&PointedOutput> for UtreexoNodeHash { fn from(pointed_output: &PointedOutput) -> Self { Self::new(hash(pointed_output)) } @@ -572,7 +577,7 @@ pub struct PointedOutputRef<'a> { pub output: &'a Output, } -impl From> for BitcoinNodeHash { +impl From> for UtreexoNodeHash { fn from(pointed_output: PointedOutputRef) -> Self { Self::new(hash(&pointed_output)) } @@ -586,8 +591,8 @@ pub struct Transaction { pub inputs: Vec<(OutPoint, Hash)>, /// Utreexo proof for inputs #[borsh(skip)] - #[schema(value_type = crate::types::schema::UtreexoProof)] - pub proof: Proof, + #[schema(value_type = schema::UtreexoProof)] + pub proof: UtreexoProof, pub outputs: Vec, } @@ -610,16 +615,6 @@ pub struct SpentOutput { pub inpoint: InPoint, } -#[derive(Debug, Error)] -pub enum ComputeFeeError { - #[error("underfunded (value in < value out)")] - Underfunded, - #[error("value in overflow")] - ValueInOverflow(#[source] AmountOverflowError), - #[error("value out overflow")] - ValueOutOverflow(#[source] AmountOverflowError), -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FilledTransaction { pub transaction: Transaction, diff --git a/types/util.rs b/types/util.rs new file mode 100644 index 00000000..fd4238bc --- /dev/null +++ b/types/util.rs @@ -0,0 +1,112 @@ +//! Utility types and functions for this crate + +/// Borsh encoding and decoding +pub(crate) mod borsh { + /// Borsh encoding + pub mod serialize { + use borsh::BorshSerialize; + + use crate::{ + UtreexoNodeHash, + authorization::{Signature, VerifyingKey}, + }; + + pub fn bitcoin_block_hash( + block_hash: &bitcoin::BlockHash, + writer: &mut W, + ) -> borsh::io::Result<()> + where + W: borsh::io::Write, + { + let bytes: &[u8; 32] = block_hash.as_ref(); + BorshSerialize::serialize(bytes, writer) + } + + pub fn signature( + sig: &Signature, + writer: &mut W, + ) -> borsh::io::Result<()> + where + W: borsh::io::Write, + { + borsh::BorshSerialize::serialize(&sig.to_bytes(), writer) + } + + pub fn utreexo_node_hash( + node_hash: &UtreexoNodeHash, + writer: &mut W, + ) -> borsh::io::Result<()> + where + W: borsh::io::Write, + { + let bytes: &[u8; 32] = node_hash; + BorshSerialize::serialize(bytes, writer) + } + + pub fn utreexo_roots( + roots: &[UtreexoNodeHash], + writer: &mut W, + ) -> borsh::io::Result<()> + where + W: borsh::io::Write, + { + #[derive(BorshSerialize)] + #[repr(transparent)] + struct SerializeUtreexoNodeHash<'a>( + #[borsh(serialize_with = "utreexo_node_hash")] + &'a UtreexoNodeHash, + ); + let roots: Vec = + roots.iter().map(SerializeUtreexoNodeHash).collect(); + BorshSerialize::serialize(&roots, writer) + } + + pub fn verifying_key( + vk: &VerifyingKey, + writer: &mut W, + ) -> borsh::io::Result<()> + where + W: borsh::io::Write, + { + borsh::BorshSerialize::serialize(&vk.to_bytes(), writer) + } + } +} + +/// Serde adapters +pub(crate) mod serde { + /// (de)serialize as hex strings for human-readable forms like json, + /// and default serialization for non human-readable formats like bincode + pub mod hexstr_human_readable { + use hex::{FromHex, ToHex}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + data: T, + serializer: S, + ) -> Result + where + S: Serializer, + T: Serialize + ToHex, + { + if serializer.is_human_readable() { + hex::serde::serialize(data, serializer) + } else { + data.serialize(serializer) + } + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: Deserialize<'de> + FromHex, + ::Error: std::fmt::Display, + { + if deserializer.is_human_readable() { + hex::serde::deserialize(deserializer) + } else { + T::deserialize(deserializer) + } + } + } +} diff --git a/types/wallet.rs b/types/wallet.rs new file mode 100644 index 00000000..ab52bd83 --- /dev/null +++ b/types/wallet.rs @@ -0,0 +1,16 @@ +use bitcoin::Amount; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, ToSchema)] +pub struct Balance { + #[serde(rename = "total_sats", with = "bitcoin::amount::serde::as_sat")] + #[schema(value_type = u64)] + pub total: Amount, + #[serde( + rename = "available_sats", + with = "bitcoin::amount::serde::as_sat" + )] + #[schema(value_type = u64)] + pub available: Amount, +} From 284b76f0f0d7bb9412f54cda0ec9a0142fb256e9 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 24 Jul 2026 21:08:16 +0800 Subject: [PATCH 05/18] hex -> const-hex --- Cargo.lock | 46 +++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 +- lib/Cargo.toml | 2 +- lib/net/peer/error.rs | 2 +- lib/net/peer/mod.rs | 4 ++-- lib/state/error.rs | 4 ++-- lib/types/proto.rs | 13 ++++++------ types/Cargo.toml | 2 +- types/address.rs | 2 +- types/hashes.rs | 18 ++++++++--------- types/util.rs | 8 ++++---- 11 files changed, 73 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2688316..139fd096 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,6 +1184,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -5038,6 +5050,21 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "prost" version = "0.14.3" @@ -5343,6 +5370,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -6521,6 +6557,7 @@ dependencies = [ "borsh", "byteorder", "bytes", + "const-hex", "ed25519-dalek", "ed25519-dalek-bip32", "educe", @@ -6530,7 +6567,6 @@ dependencies = [ "governor", "hashlink 0.10.0", "heed", - "hex", "jsonrpsee 0.25.1", "nonempty 0.11.0", "parking_lot", @@ -6669,10 +6705,10 @@ dependencies = [ "blake3", "borsh", "clap", + "const-hex", "ed25519-dalek", "hashlink 0.10.0", "heed", - "hex", "merkle-cbt", "rayon", "rustreexo", @@ -7166,6 +7202,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.9.0" diff --git a/Cargo.toml b/Cargo.toml index 2be7b983..6bf3f92e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ borsh = "1.3.1" byteorder = "1.4.3" bytes = "1.4.0" clap = "4.5.4" +const-hex = "1.19.1" dirs = "5.0.1" dotenvy = "0.15.7" ed25519-dalek = "2.1.1" @@ -35,7 +36,6 @@ futures = { version = "0.3.30", default-features = false } governor = { version = "0.8.1", default-features = false } hashlink = "0.10.0" heed = "0.22.0" -hex = "0.4.3" http = "1.2.0" human-size = "0.4.3" include_path = "0.1.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 81f3abbd..a3af33de 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -20,6 +20,7 @@ bitcoin = { workspace = true } borsh = { workspace = true } byteorder = { workspace = true } bytes = { workspace = true } +const-hex = { workspace = true } ed25519-dalek = { workspace = true } ed25519-dalek-bip32 = { workspace = true } educe = { workspace = true, features = ["Clone", "Debug"] } @@ -29,7 +30,6 @@ futures = { workspace = true, features = ["alloc"] } governor = { workspace = true, features = ["quanta", "std"] } hashlink = { workspace = true, features = ["serde_impl"] } heed = { workspace = true } -hex = { workspace = true } jsonrpsee = { workspace = true } nonempty = { workspace = true, features = ["serialize"] } parking_lot = { workspace = true } diff --git a/lib/net/peer/error.rs b/lib/net/peer/error.rs index fee4ff42..58e102dd 100644 --- a/lib/net/peer/error.rs +++ b/lib/net/peer/error.rs @@ -71,7 +71,7 @@ pub(in crate::net::peer) mod connection { #[derive(Debug, Error)] pub enum Receive { - #[error("received incorrect magic: {}", hex::encode(.0))] + #[error("received incorrect magic: {}", const_hex::encode(.0))] BadMagic(crate::net::peer::message::MagicBytes), #[error("bincode error")] Bincode(#[from] bincode::Error), diff --git a/lib/net/peer/mod.rs b/lib/net/peer/mod.rs index 938e41b5..cb49641f 100644 --- a/lib/net/peer/mod.rs +++ b/lib/net/peer/mod.rs @@ -87,13 +87,13 @@ impl From<&PeerState> for PeerStateId { impl std::fmt::Debug for PeerStateId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - hex::encode(self.0).fmt(f) + const_hex::encode(self.0).fmt(f) } } impl std::fmt::Display for PeerStateId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - hex::encode(self.0).fmt(f) + const_hex::encode(self.0).fmt(f) } } diff --git a/lib/state/error.rs b/lib/state/error.rs index 40226143..4f551c81 100644 --- a/lib/state/error.rs +++ b/lib/state/error.rs @@ -178,9 +178,9 @@ pub enum Error { UtxoDoubleSpent, #[error( "Computed Utxo hash ({}) for input ({}) does not match input hash ({})", - hex::encode(.computed), + const_hex::encode(.computed), .outpoint, - hex::encode(.input_hash), + const_hex::encode(.input_hash), )] UtxoHashMismatch { computed: crate::types::Hash, diff --git a/lib/types/proto.rs b/lib/types/proto.rs index bc52c3e6..16da2504 100644 --- a/lib/types/proto.rs +++ b/lib/types/proto.rs @@ -170,7 +170,7 @@ pub mod common { let Self { hex } = self; let hex = hex.ok_or_else(|| super::Error::missing_field::("hex"))?; - hex::decode(&hex).map_err(|_err| { + const_hex::decode(&hex).map_err(|_err| { super::Error::invalid_field_value::(field_name, &hex) }) } @@ -186,7 +186,7 @@ pub mod common { let Self { hex } = self; let hex = hex.ok_or_else(|| super::Error::missing_field::("hex"))?; - let bytes = hex::decode(&hex).map_err(|_err| { + let bytes = const_hex::decode(&hex).map_err(|_err| { super::Error::invalid_field_value::(field_name, &hex) })?; T::try_from_slice(&bytes).map_err(|_err| { @@ -196,7 +196,7 @@ pub mod common { pub fn encode(value: &T) -> Self where - T: hex::ToHex, + T: const_hex::ToHexExt, { let hex = value.encode_hex(); Self { hex: Some(hex) } @@ -216,7 +216,7 @@ pub mod common { let hex = hex .as_ref() .ok_or_else(|| super::Error::missing_field::("hex"))?; - let mut bytes = hex::decode(hex).map_err(|_| { + let mut bytes = const_hex::decode(hex).map_err(|_| { super::Error::invalid_field_value::(field_name, hex) })?; bytes.reverse(); @@ -246,7 +246,7 @@ pub mod common { let mut bytes = bitcoin::consensus::encode::serialize(value); bytes.reverse(); Self { - hex: Some(hex::encode(bytes)), + hex: Some(const_hex::encode(bytes)), } } } @@ -444,7 +444,8 @@ pub mod mainchain { Ok(address_str) => address_str, Err(_) => { tracing::warn!( - address_bytes = hex::encode(address_bytes), + address_bytes = + const_hex::encode(address_bytes), "Ignoring invalid deposit address" ); break 'address Address::ALL_ZEROS; diff --git a/types/Cargo.toml b/types/Cargo.toml index de799a18..6bf0c0dd 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -5,10 +5,10 @@ bitcoin = { workspace = true, features = ["serde"] } blake3 = { workspace = true } borsh = { workspace = true, features = ["derive"] } clap = { workspace = true, features = ["derive"], optional = true } +const-hex = { workspace = true, features = ["serde"] } ed25519-dalek = { workspace = true, features = ["batch", "serde"] } hashlink = { workspace = true, features = ["serde_impl"] } heed = { workspace = true, optional = true } -hex = { workspace = true, features = ["serde"] } merkle-cbt = { workspace = true } rayon = { workspace = true } rustreexo = { workspace = true, features = ["with-serde"] } diff --git a/types/address.rs b/types/address.rs index e0d9be49..cbc954b0 100644 --- a/types/address.rs +++ b/types/address.rs @@ -24,7 +24,7 @@ impl Address { let prefix = format!("s{}_{}_", THIS_SIDECHAIN, self.as_base58()); let prefix_digest = sha256::Hash::hash(prefix.as_bytes()).to_byte_array(); - format!("{prefix}{}", hex::encode(&prefix_digest[..3])) + format!("{prefix}{}", const_hex::encode(&prefix_digest[..3])) } } diff --git a/types/hashes.rs b/types/hashes.rs index c4a24006..61f39843 100644 --- a/types/hashes.rs +++ b/types/hashes.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use bitcoin::hashes::Hash as _; use borsh::{BorshDeserialize, BorshSerialize}; -use hex::FromHex; +use const_hex::FromHex; use serde::{Deserialize, Serialize}; use crate::util::serde::hexstr_human_readable; @@ -55,18 +55,18 @@ impl From for bitcoin::BlockHash { impl std::fmt::Display for BlockHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } impl std::fmt::Debug for BlockHash { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } impl FromStr for BlockHash { - type Err = hex::FromHexError; + type Err = const_hex::FromHexError; fn from_str(s: &str) -> Result { Hash::from_hex(s).map(Self) } @@ -116,13 +116,13 @@ impl From for Hash { impl std::fmt::Display for MerkleRoot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } impl std::fmt::Debug for MerkleRoot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } @@ -184,18 +184,18 @@ impl<'a> From<&'a Txid> for &'a Hash { impl std::fmt::Display for Txid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } impl std::fmt::Debug for Txid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) + write!(f, "{}", const_hex::encode(self.0)) } } impl FromStr for Txid { - type Err = hex::FromHexError; + type Err = const_hex::FromHexError; fn from_str(s: &str) -> Result { Hash::from_hex(s).map(Self) } diff --git a/types/util.rs b/types/util.rs index fd4238bc..ab5f0a42 100644 --- a/types/util.rs +++ b/types/util.rs @@ -78,7 +78,7 @@ pub(crate) mod serde { /// (de)serialize as hex strings for human-readable forms like json, /// and default serialization for non human-readable formats like bincode pub mod hexstr_human_readable { - use hex::{FromHex, ToHex}; + use const_hex::{FromHex, ToHexExt}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub fn serialize( @@ -87,10 +87,10 @@ pub(crate) mod serde { ) -> Result where S: Serializer, - T: Serialize + ToHex, + T: Serialize + ToHexExt, { if serializer.is_human_readable() { - hex::serde::serialize(data, serializer) + data.encode_hex().serialize(serializer) } else { data.serialize(serializer) } @@ -103,7 +103,7 @@ pub(crate) mod serde { ::Error: std::fmt::Display, { if deserializer.is_human_readable() { - hex::serde::deserialize(deserializer) + const_hex::serde::deserialize(deserializer) } else { T::deserialize(deserializer) } From c10287f4489e37f0b9f605981524a659e2be4086 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 24 Jul 2026 21:29:10 +0800 Subject: [PATCH 06/18] types/hashes: compute more efficiently --- Cargo.lock | 1 - Cargo.toml | 1 - types/Cargo.toml | 1 - types/hashes.rs | 26 +++++++++++--------------- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 139fd096..47722811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6715,7 +6715,6 @@ dependencies = [ "semver", "serde", "serde_with", - "smallvec", "strum 0.27.2", "thiserror 2.0.18", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 6bf3f92e..158f72fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,6 @@ serde_json = "1.0.113" serde_path_to_error = "0.1.20" serde_with = "3.4.0" shlex = "1.3.0" -smallvec = "1.13.2" sneed = "0.0.22" strum = { version = "0.27.2", features = ["derive"] } temp-dir = "0.2.0" diff --git a/types/Cargo.toml b/types/Cargo.toml index 6bf0c0dd..0ccd96d1 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -15,7 +15,6 @@ rustreexo = { workspace = true, features = ["with-serde"] } semver = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_with = { workspace = true } -smallvec = { workspace = true, features = ["write"] } strum = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/types/hashes.rs b/types/hashes.rs index 61f39843..2578de36 100644 --- a/types/hashes.rs +++ b/types/hashes.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use bitcoin::hashes::Hash as _; +use blake3::Hasher; use borsh::{BorshDeserialize, BorshSerialize}; use const_hex::FromHex; use serde::{Deserialize, Serialize}; @@ -231,9 +232,10 @@ pub fn hash(data: &T) -> Hash where T: BorshSerialize + ?Sized, { - let data_serialized = borsh::to_vec(data) + let mut hasher = blake3::Hasher::new(); + let () = borsh::to_writer(&mut hasher, data) .expect("failed to serialize with borsh to compute a hash"); - blake3::hash(&data_serialized).into() + hasher.finalize().into() } /// Optimized hash function that reuses a thread-local scratch buffer @@ -243,22 +245,16 @@ pub fn hash_with_scratch_buffer(data: &T) -> Hash where T: BorshSerialize + ?Sized, { - use smallvec::SmallVec; - thread_local! { - // Thread-local scratch buffer that starts with 256 bytes on the stack - // and grows as needed. This avoids heap allocations for most hashes. - static SCRATCH_BUFFER: std::cell::RefCell> = - std::cell::RefCell::new(SmallVec::new()); + static HASHER: std::cell::RefCell = + std::cell::RefCell::new(Hasher::new()); } - SCRATCH_BUFFER.with(|buffer| { - let mut buffer = buffer.borrow_mut(); - buffer.clear(); - - borsh::to_writer(&mut *buffer, data) + HASHER.with(|hasher| { + let mut hasher = hasher.borrow_mut(); + hasher.reset(); + borsh::to_writer(&mut *hasher, data) .expect("failed to serialize with borsh to compute a hash"); - - blake3::hash(&buffer).into() + hasher.finalize().into() }) } From a195d671f83f054f11be85eddf6301b9f4aad1c4 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 28 Jul 2026 13:57:12 +0800 Subject: [PATCH 07/18] RPC: sign and broadcast txs created via `create_*` --- app/rpc_server.rs | 12 ++++++++---- cli/lib.rs | 13 ++++++------- integration_tests/setup.rs | 24 +++++++++--------------- rpc-api/lib.rs | 9 ++++----- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 6eb97f04..92f04fc8 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -65,7 +65,7 @@ impl RpcServer for RpcServerImpl { dest: Address, value_sats: u64, fee_sats: u64, - ) -> RpcResult { + ) -> RpcResult { let accumulator = self.app.node.get_tip_accumulator().map_err(custom_err)?; let tx = self @@ -78,7 +78,9 @@ impl RpcServer for RpcServerImpl { Amount::from_sat(fee_sats), ) .map_err(custom_err)?; - Ok(tx) + let txid = tx.txid(); + let () = self.app.sign_and_send(tx).map_err(custom_err)?; + Ok(txid) } async fn create_withdrawal( @@ -87,7 +89,7 @@ impl RpcServer for RpcServerImpl { amount_sats: u64, fee_sats: u64, mainchain_fee_sats: u64, - ) -> RpcResult { + ) -> RpcResult { let accumulator = self.app.node.get_tip_accumulator().map_err(custom_err)?; let tx = self @@ -101,7 +103,9 @@ impl RpcServer for RpcServerImpl { Amount::from_sat(fee_sats), ) .map_err(custom_err)?; - Ok(tx) + let txid = tx.txid(); + let () = self.app.sign_and_send(tx).map_err(custom_err)?; + Ok(txid) } async fn connect_peer(&self, addr: SocketAddr) -> RpcResult<()> { diff --git a/cli/lib.rs b/cli/lib.rs index f687944e..5772a384 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -37,8 +37,7 @@ pub enum Command { #[arg(long)] fee_sats: u64, }, - /// Create a tx that transfers funds to the specified address, - /// without signing it + /// Create a tx that transfers funds to the specified address CreateTransfer { dest: Address, #[arg(long)] @@ -47,7 +46,7 @@ pub enum Command { fee_sats: u64, }, /// Creates a tx that initiates a withdrawal to the specified mainchain - /// address, without signing it + /// address CreateWithdrawal { mainchain_address: bitcoin::Address, #[arg(long)] @@ -175,10 +174,10 @@ where value_sats, fee_sats, } => { - let tx = rpc_client + let txid = rpc_client .create_transfer(dest, value_sats, fee_sats) .await?; - serde_json::to_string_pretty(&tx)? + format!("{txid}") } Command::CreateWithdrawal { mainchain_address, @@ -186,7 +185,7 @@ where fee_sats, mainchain_fee_sats, } => { - let tx = rpc_client + let txid = rpc_client .create_withdrawal( mainchain_address, amount_sats, @@ -194,7 +193,7 @@ where mainchain_fee_sats, ) .await?; - serde_json::to_string_pretty(&tx)? + format!("{txid}") } Command::FormatDepositAddress { address } => { rpc_client.format_deposit_address(address).await? diff --git a/integration_tests/setup.rs b/integration_tests/setup.rs index 4004f54f..b6d76934 100644 --- a/integration_tests/setup.rs +++ b/integration_tests/setup.rs @@ -249,21 +249,15 @@ impl Sidechain for PostSetup { fee: bitcoin::Amount, ) -> Result { - { - let withdrawal_tx = self - .rpc_client - .create_withdrawal( - receive_address.as_unchecked().clone(), - value.to_sat(), - 0, - fee.to_sat(), - ) - .await?; - let _signed_withdrawal_tx = self - .rpc_client - .sign_transaction(withdrawal_tx, Some(true)) - .await?; - } + let _txid = self + .rpc_client + .create_withdrawal( + receive_address.as_unchecked().clone(), + value.to_sat(), + 0, + fee.to_sat(), + ) + .await?; let blocks_to_mine = 'blocks_to_mine: { use thunder::state::WITHDRAWAL_BUNDLE_FAILURE_GAP; let block_count = self.rpc_client.getblockcount().await?; diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index eb261819..42578b28 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -54,18 +54,17 @@ pub trait Rpc { fee_sats: u64, ) -> RpcResult; - /// Create a tx that transfers funds to the specified address, - /// without signing it + /// Create a tx that transfers funds to the specified address #[method(name = "create_transfer")] async fn create_transfer( &self, dest: Address, value_sats: u64, fee_sats: u64, - ) -> RpcResult; + ) -> RpcResult; /// Creates a tx that initiates a withdrawal to the specified mainchain - /// address, without signing it + /// address #[method(name = "create_withdrawal")] async fn create_withdrawal( &self, @@ -78,7 +77,7 @@ pub trait Rpc { amount_sats: u64, fee_sats: u64, mainchain_fee_sats: u64, - ) -> RpcResult; + ) -> RpcResult; /// Format a deposit address #[method(name = "format_deposit_address")] From 2ed7a06e302ad546076516577e608533a00e01a4 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Mon, 27 Jul 2026 21:25:40 +0800 Subject: [PATCH 08/18] RPC: add get_stxos/get_utxos methods --- app/rpc_server.rs | 35 +++++++++++++++++++++++++++++++++-- cli/lib.rs | 20 ++++++++++++++++++++ lib/node/mod.rs | 12 ++++++++++++ lib/state/mod.rs | 19 +++++++++++++++++++ rpc-api/lib.rs | 20 +++++++++++++++++--- types/hashes.rs | 6 +++++- types/lib.rs | 2 +- types/transaction.rs | 9 ++++++--- 8 files changed, 113 insertions(+), 10 deletions(-) diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 92f04fc8..36b2a46f 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{collections::HashSet, net::SocketAddr}; use bitcoin::Amount; use jsonrpsee::{ @@ -7,7 +7,8 @@ use jsonrpsee::{ types::ErrorObject, }; use thunder::types::{ - Address, PointedOutput, Txid, WithdrawalBundle, net::Peer, wallet::Balance, + Address, Pointed, PointedOutput, SpentOutput, Txid, WithdrawalBundle, + net::Peer, wallet::Balance, }; use thunder_app_rpc_api::{GetTransactionResponse, RpcServer}; use tower_http::{ @@ -189,6 +190,21 @@ impl RpcServer for RpcServerImpl { self.app.wallet.get_new_address().map_err(custom_err) } + async fn get_stxos( + &self, + addresses: HashSet
, + ) -> RpcResult>> { + let res = self + .app + .node + .get_stxos_by_addresses(&addresses) + .map_err(custom_err)? + .into_iter() + .map(|(outpoint, output)| Pointed { outpoint, output }) + .collect(); + Ok(res) + } + async fn get_transaction( &self, txid: Txid, @@ -202,6 +218,21 @@ impl RpcServer for RpcServerImpl { Ok(res) } + async fn get_utxos( + &self, + addresses: HashSet
, + ) -> RpcResult> { + let res = self + .app + .node + .get_utxos_by_addresses(&addresses) + .map_err(custom_err)? + .into_iter() + .map(|(outpoint, output)| PointedOutput { outpoint, output }) + .collect(); + Ok(res) + } + async fn get_wallet_addresses(&self) -> RpcResult> { let addrs = self.app.wallet.get_addresses().map_err(custom_err)?; let mut res: Vec<_> = addrs.into_iter().collect(); diff --git a/cli/lib.rs b/cli/lib.rs index 5772a384..5dde9dbd 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -77,8 +77,18 @@ pub enum Command { }, /// Get a new address GetNewAddress, + /// Get stxos for addresses + GetStxos { + #[arg(required = true)] + addresses: Vec
, + }, /// Get transaction by txid GetTransaction { txid: Txid }, + /// Get utxos for addresses + GetUtxos { + #[arg(required = true)] + addresses: Vec
, + }, /// Get wallet addresses, sorted by base58 encoding GetWalletAddresses, /// Get wallet UTXOs @@ -224,10 +234,20 @@ where let address = rpc_client.get_new_address().await?; format!("{address}") } + Command::GetStxos { addresses } => { + let addresses = addresses.into_iter().collect(); + let stxos = rpc_client.get_stxos(addresses).await?; + serde_json::to_string_pretty(&stxos)? + } Command::GetTransaction { txid } => { let tx_info = rpc_client.get_transaction(txid).await?; serde_json::to_string_pretty(&tx_info)? } + Command::GetUtxos { addresses } => { + let addresses = addresses.into_iter().collect(); + let utxos = rpc_client.get_utxos(addresses).await?; + serde_json::to_string_pretty(&utxos)? + } Command::GetWalletAddresses => { let addresses = rpc_client.get_wallet_addresses().await?; serde_json::to_string_pretty(&addresses)? diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 04bda213..7d780b9b 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -297,6 +297,18 @@ where Ok(spent) } + pub fn get_stxos_by_addresses( + &self, + addresses: &HashSet
, + ) -> Result, Error> { + let rotxn = self.env.read_txn()?; + let stxos = self + .state + .get_stxos_by_addresses(&rotxn, addresses) + .map_err(DbError::from)?; + Ok(stxos) + } + pub fn get_utxos_by_addresses( &self, addresses: &HashSet
, diff --git a/lib/state/mod.rs b/lib/state/mod.rs index 47920e43..49434a92 100644 --- a/lib/state/mod.rs +++ b/lib/state/mod.rs @@ -175,6 +175,25 @@ impl State { Ok(height) } + pub fn get_stxos_by_addresses( + &self, + rotxn: &RoTxn, + addresses: &HashSet
, + ) -> Result, db_error::Iter> { + let stxos: HashMap = self + .stxos + .iter(rotxn)? + .filter_map(|(key, output)| { + if addresses.contains(&output.output.address) { + Ok(Some((key.into(), output))) + } else { + Ok(None) + } + }) + .collect()?; + Ok(stxos) + } + pub fn get_utxos( &self, rotxn: &RoTxn, diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 42578b28..73ec176d 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -1,14 +1,14 @@ //! RPC API -use std::net::SocketAddr; +use std::{collections::HashSet, net::SocketAddr}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use l2l_openapi::open_api; use serde::{Deserialize, Serialize}; use thunder_types::{ Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, - OutputContent, PointedOutput, Transaction, Txid, WithdrawalBundle, - net::Peer, schema as thunder_schema, wallet::Balance, + OutputContent, Pointed, PointedOutput, SpentOutput, Transaction, Txid, + WithdrawalBundle, net::Peer, schema as thunder_schema, wallet::Balance, }; use utoipa::ToSchema; @@ -140,6 +140,13 @@ pub trait Rpc { #[method(name = "get_new_address")] async fn get_new_address(&self) -> RpcResult
; + /// Get stxos for addresses + #[method(name = "get_stxos")] + async fn get_stxos( + &self, + addresses: HashSet
, + ) -> RpcResult>>; + /// Get transaction by txid #[method(name = "get_transaction")] async fn get_transaction( @@ -147,6 +154,13 @@ pub trait Rpc { txid: Txid, ) -> RpcResult>; + /// Get utxos for addresses + #[method(name = "get_utxos")] + async fn get_utxos( + &self, + addresses: HashSet
, + ) -> RpcResult>; + /// Get wallet addresses, sorted by base58 encoding #[method(name = "get_wallet_addresses")] async fn get_wallet_addresses(&self) -> RpcResult>; diff --git a/types/hashes.rs b/types/hashes.rs index 2578de36..009b1c40 100644 --- a/types/hashes.rs +++ b/types/hashes.rs @@ -5,6 +5,7 @@ use blake3::Hasher; use borsh::{BorshDeserialize, BorshSerialize}; use const_hex::FromHex; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::util::serde::hexstr_human_readable; @@ -216,8 +217,11 @@ impl utoipa::ToSchema for Txid { } } -#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive( + Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, +)] #[repr(transparent)] +#[schema(value_type = crate::schema::BitcoinOutPoint)] #[serde(transparent)] pub struct M6id(pub bitcoin::Txid); diff --git a/types/lib.rs b/types/lib.rs index dd238d00..375e574c 100644 --- a/types/lib.rs +++ b/types/lib.rs @@ -34,7 +34,7 @@ pub mod transaction; pub use transaction::{ Authorized, AuthorizedTransaction, Content as OutputContent, FilledTransaction, GetAddress, GetValue, InPoint, OutPoint, OutPointKey, - Output, PointedOutput, PointedOutputRef, SpentOutput, Transaction, + Output, Pointed, PointedOutput, PointedOutputRef, SpentOutput, Transaction, }; mod util; pub mod wallet; diff --git a/types/transaction.rs b/types/transaction.rs index 6d333c71..7af8380c 100644 --- a/types/transaction.rs +++ b/types/transaction.rs @@ -292,7 +292,9 @@ mod test { } /// Reference to a tx input. -#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive( + Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, +)] pub enum InPoint { /// Transaction input Regular { @@ -558,10 +560,11 @@ impl GetValue for Output { Serialize, ToSchema, )] -pub struct PointedOutput { +pub struct Pointed { pub outpoint: OutPoint, pub output: Output, } +pub type PointedOutput = Pointed; impl From<&PointedOutput> for UtreexoNodeHash { fn from(pointed_output: &PointedOutput) -> Self { @@ -609,7 +612,7 @@ impl Transaction { } /// Representation of a spent output -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)] pub struct SpentOutput { pub output: Output, pub inpoint: InPoint, From a10906871727afac209991752fc63aecda73b7ef Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Mon, 27 Jul 2026 21:56:11 +0800 Subject: [PATCH 09/18] RPC: regenerate utreexo proofs in `submit_transaction` --- app/app.rs | 13 +++++++------ app/rpc_server.rs | 8 ++++---- lib/node/mod.rs | 22 ++++++++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/app.rs b/app/app.rs index a2289520..5d7323af 100644 --- a/app/app.rs +++ b/app/app.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{borrow::BorrowMut, collections::HashMap, sync::Arc}; use fallible_iterator::FallibleIterator as _; use futures::{StreamExt, TryFutureExt}; @@ -280,10 +280,11 @@ impl App { update(self.node.as_ref(), &mut self.utxos.write(), &self.wallet) } - pub fn submit_transaction( - &self, - tx: &thunder::types::AuthorizedTransaction, - ) -> Result<(), Error> { + /// Regenerate proofs and submit transaction + pub fn submit_transaction(&self, tx: Tx) -> Result<(), Error> + where + Tx: BorrowMut, + { self.node.submit_transaction(tx)?; let () = self.update()?; Ok(()) @@ -291,7 +292,7 @@ impl App { pub fn sign_and_send(&self, tx: Transaction) -> Result<(), Error> { let authorized_transaction = self.wallet.authorize(tx)?; - self.submit_transaction(&authorized_transaction) + self.submit_transaction(authorized_transaction) } pub async fn get_new_main_address( diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 36b2a46f..ae18c06b 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -332,12 +332,12 @@ impl RpcServer for RpcServerImpl { transaction: thunder::types::Transaction, broadcast: Option, ) -> RpcResult { - let authorized = + let mut authorized = self.app.wallet.authorize(transaction).map_err(custom_err)?; if let Some(true) = broadcast { let () = self .app - .submit_transaction(&authorized) + .submit_transaction(&mut authorized) .map_err(custom_err)?; } Ok(authorized) @@ -345,11 +345,11 @@ impl RpcServer for RpcServerImpl { async fn submit_transaction( &self, - transaction: thunder::types::AuthorizedTransaction, + mut transaction: thunder::types::AuthorizedTransaction, ) -> RpcResult { let () = self .app - .submit_transaction(&transaction) + .submit_transaction(&mut transaction) .map_err(custom_err)?; Ok(transaction.transaction.txid()) } diff --git a/lib/node/mod.rs b/lib/node/mod.rs index 7d780b9b..f0f39ace 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -1,4 +1,5 @@ use std::{ + borrow::BorrowMut, collections::{HashMap, HashSet}, fmt::Debug, net::SocketAddr, @@ -244,17 +245,26 @@ where Ok(self.state.try_get_tip(&rotxn)?) } - pub fn submit_transaction( + /// Regenerate proofs and submit transaction + pub fn submit_transaction( &self, - transaction: &AuthorizedTransaction, - ) -> Result<(), Error> { + mut transaction: Tx, + ) -> Result<(), Error> + where + Tx: BorrowMut, + { { let mut rotxn = self.env.write_txn().map_err(EnvError::from)?; - self.state.validate_transaction(&rotxn, transaction)?; - self.mempool.put(&mut rotxn, transaction)?; + self.state.regenerate_proof( + &rotxn, + &mut transaction.borrow_mut().transaction, + )?; + self.state + .validate_transaction(&rotxn, transaction.borrow())?; + self.mempool.put(&mut rotxn, transaction.borrow())?; rotxn.commit().map_err(RwTxnError::from)?; } - self.net.push_tx(Default::default(), transaction); + self.net.push_tx(Default::default(), transaction.borrow()); Ok(()) } From 5627d8c85c14ba07a5a1ea7776b7e34de3b13481 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Mon, 27 Jul 2026 22:41:39 +0800 Subject: [PATCH 10/18] update bip300301_enforcer --- Cargo.lock | 224 +++++++++++++++++++++++++-------------------- Cargo.toml | 2 +- app/app.rs | 72 ++++++++++----- bip300301_enforcer | 2 +- lib/build.rs | 7 +- lib/miner.rs | 17 +++- lib/types/proto.rs | 53 +++++++---- 7 files changed, 234 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47722811..39a1242b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" + [[package]] name = "base64ct" version = "1.8.3" @@ -519,7 +525,7 @@ dependencies = [ "serde_json", "strum 0.28.0", "temp-dir", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", ] @@ -551,7 +557,7 @@ dependencies = [ "error-fatality", "fallible-iterator", "futures", - "hashlink 0.12.0", + "hashlink 0.12.1", "heed", "heed-types", "hex", @@ -572,7 +578,7 @@ dependencies = [ "serde", "serde_json", "sneed", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rusqlite", "tokio-stream", @@ -659,12 +665,12 @@ dependencies = [ [[package]] name = "bitcoin-jsonrpsee" version = "0.1.1" -source = "git+https://github.com/LayerTwo-Labs/bitcoin-jsonrpsee.git?rev=4810db2112d1bc437b8c4b78520f79e5e9ce8630#4810db2112d1bc437b8c4b78520f79e5e9ce8630" +source = "git+https://github.com/LayerTwo-Labs/bitcoin-jsonrpsee.git?rev=e5e4d8eeef6141bc2d8364da3850117efd5f5e4e#e5e4d8eeef6141bc2d8364da3850117efd5f5e4e" dependencies = [ - "base64 0.22.1", + "base64 0.23.0", "bitcoin", - "educe", - "hashlink 0.12.0", + "educe 0.7.4", + "hashlink 0.12.1", "hex", "http", "jsonrpsee 0.26.0", @@ -672,7 +678,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", ] @@ -805,6 +811,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "buffa" version = "0.8.1" @@ -820,7 +835,7 @@ dependencies = [ "serde", "serde_json", "smoothutf8", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -845,7 +860,7 @@ dependencies = [ "bytes", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -1162,7 +1177,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tower", @@ -1408,17 +1423,17 @@ dependencies = [ [[package]] name = "cusf-enforcer-mempool" version = "0.4.0" -source = "git+https://github.com/LayerTwo-Labs/cusf-enforcer-mempool.git?rev=cbf0f2057f311f936c8e5e15520da3b4193feb51#cbf0f2057f311f936c8e5e15520da3b4193feb51" +source = "git+https://github.com/LayerTwo-Labs/cusf-enforcer-mempool.git?rev=2856cb2e95f4978101334dee81e3a2d207a1c0b6#2856cb2e95f4978101334dee81e3a2d207a1c0b6" dependencies = [ "anyhow", "async-trait", "bitcoin", "bitcoin-jsonrpsee", "chrono", - "educe", + "educe 0.7.4", "either", "futures", - "hashlink 0.12.0", + "hashlink 0.12.1", "imbl", "indexmap 2.14.0", "jsonrpsee 0.26.0", @@ -1426,7 +1441,7 @@ dependencies = [ "nonempty 0.12.0", "parking_lot", "polonius-the-crab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-util", "tracing", @@ -1437,9 +1452,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", @@ -1447,11 +1462,10 @@ 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", @@ -1461,9 +1475,9 @@ dependencies = [ [[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", @@ -1696,6 +1710,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "educe" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c3e1715a2bf74bc8f68cd7bae12ff144f02669c602106ad1fa16f2ba62e646" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "eframe" version = "0.35.0" @@ -1763,7 +1789,7 @@ dependencies = [ "epaint", "log", "profiling", - "thiserror 2.0.18", + "thiserror 2.0.19", "type-map", "web-time", "wgpu", @@ -1847,18 +1873,18 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[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", @@ -2686,19 +2712,9 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", - "serde", -] - -[[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", @@ -3281,7 +3297,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -3397,7 +3413,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier 0.5.3", "soketto", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-util", @@ -3422,7 +3438,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier 0.5.3", "soketto", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-rustls", "tokio-util", @@ -3450,7 +3466,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tower", @@ -3478,7 +3494,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tower", @@ -3503,7 +3519,7 @@ dependencies = [ "rustls-platform-verifier 0.5.3", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tower", "url", @@ -3526,7 +3542,7 @@ dependencies = [ "rustls-platform-verifier 0.5.3", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tower", "url", @@ -3577,7 +3593,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tokio-util", @@ -3604,7 +3620,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tokio-stream", "tokio-util", @@ -3621,7 +3637,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3633,7 +3649,7 @@ dependencies = [ "http", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3750,9 +3766,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lender" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e306e6c6b4be049e429a93d121609257648ea8ce31d8d57ede117b85646ecca" +checksum = "71ee39582f2e2a4dd0de3c898e944f5cba919eb2f61aecaaeb5279d1e6b9ae6f" dependencies = [ "aliasable", "fallible-iterator", @@ -4096,9 +4112,9 @@ dependencies = [ [[package]] name = "monostate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4cc965c89dd0615a9e822ff8002f7633d2466143d51bd58693e4b2c75aabad" +checksum = "fdc4dfdf312bb1b22fce127237589f4ee31652bc479f3d208332f32132aaccc9" dependencies = [ "monostate-impl", "serde", @@ -4107,13 +4123,13 @@ dependencies = [ [[package]] name = "monostate-impl" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f5b99488110875b5904839d396c2cdfaf241ff6622638acb879cc7effad5de" +checksum = "a656449eeb444c77364a3033f013c51869eee320c0dff805eb53024c0dfd435b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -4153,7 +4169,7 @@ dependencies = [ "num-traits", "once_cell", "rustc-hash 1.1.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-ident", ] @@ -5142,7 +5158,7 @@ dependencies = [ "prost-reflect", "prost-types", "protox-parse", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -5154,7 +5170,7 @@ dependencies = [ "logos", "miette", "prost-types", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -5230,7 +5246,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -5253,7 +5269,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier 0.6.2", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -5614,7 +5630,7 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc" dependencies = [ - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -5998,9 +6014,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -6008,29 +6024,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -6064,11 +6080,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", "indexmap 1.9.3", @@ -6083,9 +6100,9 @@ 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", @@ -6247,7 +6264,7 @@ dependencies = [ "log", "memmap2", "rustix 1.1.3", - "thiserror 2.0.18", + "thiserror 2.0.19", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -6301,14 +6318,14 @@ version = "0.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee7a0c2e3943d649a50f22cd21198aac5355b3eee046277f0f000c465efd6d78" dependencies = [ - "educe", + "educe 0.6.0", "enum-ordinalize", "fallible-iterator", "heed", "hex", "strum 0.27.2", "syn 1.0.109", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", ] @@ -6450,6 +6467,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6508,11 +6536,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -6528,13 +6556,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -6560,12 +6588,12 @@ dependencies = [ "const-hex", "ed25519-dalek", "ed25519-dalek-bip32", - "educe", + "educe 0.6.0", "error-fatality", "fallible-iterator", "futures", "governor", - "hashlink 0.10.0", + "hashlink 0.12.1", "heed", "jsonrpsee 0.25.1", "nonempty 0.11.0", @@ -6583,7 +6611,7 @@ dependencies = [ "serde_json", "sneed", "temp-dir", - "thiserror 2.0.18", + "thiserror 2.0.19", "thunder_types", "tiny-bip39", "tokio", @@ -6619,7 +6647,7 @@ dependencies = [ "serde", "shlex", "strum 0.27.2", - "thiserror 2.0.18", + "thiserror 2.0.19", "thunder", "thunder_app_cli", "thunder_app_rpc_api", @@ -6687,7 +6715,7 @@ dependencies = [ "jsonrpsee 0.25.1", "libtest-mimic", "reserve-port", - "thiserror 2.0.18", + "thiserror 2.0.19", "thunder", "thunder_app_rpc_api", "tokio", @@ -6707,7 +6735,7 @@ dependencies = [ "clap", "const-hex", "ed25519-dalek", - "hashlink 0.10.0", + "hashlink 0.12.1", "heed", "merkle-cbt", "rayon", @@ -6716,7 +6744,7 @@ dependencies = [ "serde", "serde_with", "strum 0.27.2", - "thiserror 2.0.18", + "thiserror 2.0.19", "tracing", "utoipa", ] @@ -7193,11 +7221,11 @@ checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "ulid" -version = "1.2.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +checksum = "947dde63b6d514cc5e044edad4e0ca7261afd1099d16c83d942cb2b2f348689c" dependencies = [ - "rand 0.9.4", + "rand 0.10.1", "web-time", ] @@ -7363,7 +7391,7 @@ dependencies = [ "log", "peniko", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -7803,7 +7831,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.18", + "thiserror 2.0.19", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", @@ -7835,7 +7863,7 @@ dependencies = [ "portable-atomic-util", "raw-window-handle", "renderdoc-sys", - "thiserror 2.0.18", + "thiserror 2.0.19", "wgpu-naga-bridge", "wgpu-types", ] diff --git a/Cargo.toml b/Cargo.toml index 158f72fe..65839c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ error-fatality = "0.1.2" fallible-iterator = "0.3.0" futures = { version = "0.3.30", default-features = false } governor = { version = "0.8.1", default-features = false } -hashlink = "0.10.0" +hashlink = "0.12.1" heed = "0.22.0" http = "1.2.0" human-size = "0.4.3" diff --git a/app/app.rs b/app/app.rs index 5d7323af..54f8dfa9 100644 --- a/app/app.rs +++ b/app/app.rs @@ -11,7 +11,10 @@ use thunder::{ self, Address, FilledTransaction, OutPoint, Output, Transaction, proto::mainchain::{ self, - generated::{validator_service_server, wallet_service_server}, + generated::{ + mining_service_server, validator_service_server, + wallet_service_server, + }, }, }, wallet::{self, Wallet}, @@ -86,6 +89,11 @@ fn update( Ok(()) } +struct ProtoSupport { + miner: bool, + wallet: bool, +} + #[derive(Clone)] pub struct App { pub node: Arc, @@ -152,14 +160,14 @@ impl App { } } - /// Returns `true` if validator service AND wallet service are available, - /// `false` if only validator service is available, and error if validator + /// Returns `Ok(_)` iff validator service is available, and error if validator /// service is unavailable. async fn check_proto_support( transport: tonic::transport::channel::Channel, - ) -> Result { + ) -> Result { let mut client = HealthClient::new(transport); + let mining_service_name = mining_service_server::SERVICE_NAME; let validator_service_name = validator_service_server::SERVICE_NAME; let wallet_service_name = wallet_service_server::SERVICE_NAME; @@ -174,17 +182,26 @@ impl App { tracing::info!("Verified existence of {}", validator_service_name); - // The wallet service is optional. + // The mining and wallet services are optional. + let has_mining_service = + Self::check_status_serving(&mut client, mining_service_name) + .await?; let has_wallet_service = Self::check_status_serving(&mut client, wallet_service_name) .await?; tracing::info!( - "Checked existence of {}: {}", + %has_mining_service, + %has_wallet_service, + "Checked existence of {}, {}", + mining_service_name, wallet_service_name, - has_wallet_service ); - Ok(has_wallet_service) + let res = ProtoSupport { + miner: has_mining_service, + wallet: has_wallet_service, + }; + Ok(res) } pub fn new(config: &Config) -> Result { @@ -216,22 +233,35 @@ impl App { .unwrap() .concurrency_limit(256) .connect_lazy(); - let (cusf_mainchain, cusf_mainchain_wallet) = if runtime - .block_on(Self::check_proto_support(transport.clone())) - .map_err(|err| Error::VerifyMainchainServices { - url: Box::new(config.mainchain_grpc_url.clone()), - source: Box::new(err), - })? { - ( - mainchain::ValidatorClient::new(transport.clone()), - Some(mainchain::WalletClient::new(transport)), - ) - } else { - (mainchain::ValidatorClient::new(transport), None) + let (cusf_mainchain, cusf_mainchain_miner, cusf_mainchain_wallet) = { + let ProtoSupport { miner, wallet } = runtime + .block_on(Self::check_proto_support(transport.clone())) + .map_err(|err| Error::VerifyMainchainServices { + url: Box::new(config.mainchain_grpc_url.clone()), + source: Box::new(err), + })?; + let mining_client = if miner { + Some(mainchain::MiningClient::new(transport.clone())) + } else { + None + }; + let wallet_client = if wallet { + Some(mainchain::WalletClient::new(transport.clone())) + } else { + None + }; + let validator_client = mainchain::ValidatorClient::new(transport); + (validator_client, mining_client, wallet_client) }; let miner = cusf_mainchain_wallet .clone() - .map(|wallet| Miner::new(cusf_mainchain.clone(), wallet)) + .map(|wallet| { + Miner::new( + cusf_mainchain.clone(), + cusf_mainchain_miner.clone(), + wallet, + ) + }) .transpose()?; let local_pool = LocalPoolHandle::new(1); diff --git a/bip300301_enforcer b/bip300301_enforcer index ebd851f1..ca0bd4cb 160000 --- a/bip300301_enforcer +++ b/bip300301_enforcer @@ -1 +1 @@ -Subproject commit ebd851f1113dd34e295f487b2368ba397da8abba +Subproject commit ca0bd4cb8d72054b3ff4a6f8b4573ea3eecf8ba1 diff --git a/lib/build.rs b/lib/build.rs index d34f0f15..76d8b06c 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -31,11 +31,14 @@ where fn main() -> Result<(), Box> { const COMMON_PROTO: &str = "../bip300301_enforcer/proto/cusf/common/v1/common.proto"; + const MINING_PROTO: &str = + "../bip300301_enforcer/proto/cusf/mainchain/v1/mining.proto"; const VALIDATOR_PROTO: &str = "../bip300301_enforcer/proto/cusf/mainchain/v1/validator.proto"; const WALLET_PROTO: &str = "../bip300301_enforcer/proto/cusf/mainchain/v1/wallet.proto"; - const ALL_PROTOS: &[&str] = &[COMMON_PROTO, VALIDATOR_PROTO, WALLET_PROTO]; + const ALL_PROTOS: &[&str] = + &[COMMON_PROTO, MINING_PROTO, VALIDATOR_PROTO, WALLET_PROTO]; const INCLUDES: &[&str] = &["../bip300301_enforcer/proto"]; let file_descriptors = protox::compile(ALL_PROTOS, INCLUDES)?; let file_descriptor_path = PathBuf::from( @@ -52,7 +55,7 @@ fn main() -> Result<(), Box> { )?; let () = compile_protos_with_config( &file_descriptor_path, - &[VALIDATOR_PROTO, WALLET_PROTO], + &[VALIDATOR_PROTO, MINING_PROTO, WALLET_PROTO], INCLUDES, |config| { config diff --git a/lib/miner.rs b/lib/miner.rs index 6faec54e..0094c38b 100644 --- a/lib/miner.rs +++ b/lib/miner.rs @@ -11,11 +11,15 @@ pub enum Error { CusfMainchain(#[from] proto::Error), #[error("invalid json: {json}")] InvalidJson { json: serde_json::Value }, + #[error("No CUSF mainchain mining client")] + NoCusfMainchainMiningClient, } #[derive(Clone)] pub struct Miner { pub cusf_mainchain: mainchain::ValidatorClient, + pub cusf_mainchain_miner: + Option>, pub cusf_mainchain_wallet: mainchain::WalletClient, block: Option<(Header, Body)>, } @@ -23,10 +27,14 @@ pub struct Miner { impl Miner { pub fn new( cusf_mainchain: mainchain::ValidatorClient, + cusf_mainchain_miner: Option< + mainchain::MiningClient, + >, cusf_mainchain_wallet: mainchain::WalletClient, ) -> Result { Ok(Self { cusf_mainchain, + cusf_mainchain_miner, cusf_mainchain_wallet, block: None, }) @@ -38,7 +46,14 @@ where MainchainTransport: proto::Transport, { pub async fn generate(&mut self) -> Result<(), Error> { - let () = self.cusf_mainchain_wallet.generate_blocks(1).await?; + let Some(cusf_mainchain_miner) = self.cusf_mainchain_miner.as_mut() + else { + return Err(Error::NoCusfMainchainMiningClient); + }; + let address = self.cusf_mainchain_wallet.create_new_address().await?; + let () = cusf_mainchain_miner + .generate_to_address(1, &address) + .await?; Ok(()) } diff --git a/lib/types/proto.rs b/lib/types/proto.rs index 16da2504..5fa73b13 100644 --- a/lib/types/proto.rs +++ b/lib/types/proto.rs @@ -259,7 +259,7 @@ pub mod mainchain { self, BlockHash, Network, OutPoint, Transaction, Txid, Work, hashes::Hash as _, }; - use futures::{StreamExt as _, TryStreamExt as _, stream::BoxStream}; + use futures::{StreamExt as _, stream::BoxStream}; use hashlink::LinkedHashMap; use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; @@ -967,6 +967,39 @@ pub mod mainchain { pub struct EventStream; + #[derive(Clone, Debug)] + #[repr(transparent)] + pub struct MiningClient( + pub generated::mining_service_client::MiningServiceClient, + ); + + impl MiningClient + where + T: super::Transport, + { + pub fn new(inner: T) -> Self { + Self( + generated::mining_service_client::MiningServiceClient::::new( + inner, + ), + ) + } + + pub async fn generate_to_address( + &mut self, + blocks: u32, + address: &bitcoin::Address, + ) -> Result<(), super::Error> { + let request = generated::GenerateToAddressRequest { + blocks: Some(blocks), + address: address.assume_checked_ref().to_string(), + }; + let _resp: generated::GenerateToAddressResponse = + self.0.generate_to_address(request).await?.into_inner(); + Ok(()) + } + } + #[derive(Clone, Debug)] #[repr(transparent)] pub struct ValidatorClient( @@ -1277,23 +1310,5 @@ pub mod mainchain { })?; Ok(address) } - - pub async fn generate_blocks( - &mut self, - blocks: u32, - ) -> Result<(), super::Error> { - let request = generated::GenerateBlocksRequest { - blocks: Some(blocks), - ack_all_proposals: true, - }; - let _resp: Vec = self - .0 - .generate_blocks(request) - .await? - .into_inner() - .try_collect() - .await?; - Ok(()) - } } } From f615144dff7216b92bd5cebe65077b1b731ca503 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Mon, 27 Jul 2026 22:47:28 +0800 Subject: [PATCH 11/18] update jsonrpsee/l2l-openapi --- Cargo.lock | 236 ++++++++--------------------------------------------- Cargo.toml | 4 +- 2 files changed, 34 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39a1242b..4ffd3c0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,7 +516,7 @@ dependencies = [ "futures", "hex", "http", - "jsonrpsee 0.26.0", + "jsonrpsee", "libtest-mimic", "parking_lot", "reserve-port", @@ -563,7 +563,7 @@ dependencies = [ "hex", "http-body", "jiff", - "jsonrpsee 0.26.0", + "jsonrpsee", "miette", "nom", "nonempty 0.12.0", @@ -673,7 +673,7 @@ dependencies = [ "hashlink 0.12.1", "hex", "http", - "jsonrpsee 0.26.0", + "jsonrpsee", "monostate", "serde", "serde_json", @@ -1436,7 +1436,7 @@ dependencies = [ "hashlink 0.12.1", "imbl", "indexmap 2.14.0", - "jsonrpsee 0.26.0", + "jsonrpsee", "lender", "nonempty 0.12.0", "parking_lot", @@ -3360,67 +3360,24 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpsee" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" -dependencies = [ - "jsonrpsee-client-transport 0.25.1", - "jsonrpsee-core 0.25.1", - "jsonrpsee-http-client 0.25.1", - "jsonrpsee-proc-macros 0.25.1", - "jsonrpsee-server 0.25.1", - "jsonrpsee-types 0.25.1", - "jsonrpsee-wasm-client 0.25.1", - "jsonrpsee-ws-client 0.25.1", - "tokio", - "tracing", -] - [[package]] name = "jsonrpsee" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" dependencies = [ - "jsonrpsee-client-transport 0.26.0", - "jsonrpsee-core 0.26.0", - "jsonrpsee-http-client 0.26.0", - "jsonrpsee-proc-macros 0.26.0", - "jsonrpsee-server 0.26.0", - "jsonrpsee-types 0.26.0", - "jsonrpsee-wasm-client 0.26.0", - "jsonrpsee-ws-client 0.26.0", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", "tokio", "tracing", ] -[[package]] -name = "jsonrpsee-client-transport" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a320a3f1464e4094f780c4d48413acd786ce5627aaaecfac9e9c7431d13ae1" -dependencies = [ - "base64 0.22.1", - "futures-channel", - "futures-util", - "gloo-net", - "http", - "jsonrpsee-core 0.25.1", - "pin-project", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier 0.5.3", - "soketto", - "thiserror 2.0.19", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", - "url", -] - [[package]] name = "jsonrpsee-client-transport" version = "0.26.0" @@ -3432,7 +3389,7 @@ dependencies = [ "futures-util", "gloo-net", "http", - "jsonrpsee-core 0.26.0", + "jsonrpsee-core", "pin-project", "rustls", "rustls-pki-types", @@ -3446,34 +3403,6 @@ dependencies = [ "url", ] -[[package]] -name = "jsonrpsee-core" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" -dependencies = [ - "async-trait", - "bytes", - "futures-timer", - "futures-util", - "http", - "http-body", - "http-body-util", - "jsonrpsee-types 0.25.1", - "parking_lot", - "pin-project", - "rand 0.9.4", - "rustc-hash 2.1.1", - "serde", - "serde_json", - "thiserror 2.0.19", - "tokio", - "tokio-stream", - "tower", - "tracing", - "wasm-bindgen-futures", -] - [[package]] name = "jsonrpsee-core" version = "0.26.0" @@ -3487,7 +3416,7 @@ dependencies = [ "http", "http-body", "http-body-util", - "jsonrpsee-types 0.26.0", + "jsonrpsee-types", "parking_lot", "pin-project", "rand 0.9.4", @@ -3502,29 +3431,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "jsonrpsee-http-client" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" -dependencies = [ - "base64 0.22.1", - "http-body", - "hyper", - "hyper-rustls", - "hyper-util", - "jsonrpsee-core 0.25.1", - "jsonrpsee-types 0.25.1", - "rustls", - "rustls-platform-verifier 0.5.3", - "serde", - "serde_json", - "thiserror 2.0.19", - "tokio", - "tower", - "url", -] - [[package]] name = "jsonrpsee-http-client" version = "0.26.0" @@ -3536,8 +3442,8 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "jsonrpsee-core 0.26.0", - "jsonrpsee-types 0.26.0", + "jsonrpsee-core", + "jsonrpsee-types", "rustls", "rustls-platform-verifier 0.5.3", "serde", @@ -3548,19 +3454,6 @@ dependencies = [ "url", ] -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" -dependencies = [ - "heck 0.5.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "jsonrpsee-proc-macros" version = "0.26.0" @@ -3574,33 +3467,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "jsonrpsee-server" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" -dependencies = [ - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "jsonrpsee-core 0.25.1", - "jsonrpsee-types 0.25.1", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 2.0.19", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tracing", -] - [[package]] name = "jsonrpsee-server" version = "0.26.0" @@ -3613,8 +3479,8 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "jsonrpsee-core 0.26.0", - "jsonrpsee-types 0.26.0", + "jsonrpsee-core", + "jsonrpsee-types", "pin-project", "route-recognizer", "serde", @@ -3628,18 +3494,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "jsonrpsee-types" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" -dependencies = [ - "http", - "serde", - "serde_json", - "thiserror 2.0.19", -] - [[package]] name = "jsonrpsee-types" version = "0.26.0" @@ -3652,42 +3506,16 @@ dependencies = [ "thiserror 2.0.19", ] -[[package]] -name = "jsonrpsee-wasm-client" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b67695cbcf4653f39f8f8738925547e0e23fd9fe315bccf951097b9f6a38781" -dependencies = [ - "jsonrpsee-client-transport 0.25.1", - "jsonrpsee-core 0.25.1", - "jsonrpsee-types 0.25.1", - "tower", -] - [[package]] name = "jsonrpsee-wasm-client" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" dependencies = [ - "jsonrpsee-client-transport 0.26.0", - "jsonrpsee-core 0.26.0", - "jsonrpsee-types 0.26.0", - "tower", -] - -[[package]] -name = "jsonrpsee-ws-client" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da2694c9ff271a9d3ebfe520f6b36820e85133a51be77a3cb549fd615095261" -dependencies = [ - "http", - "jsonrpsee-client-transport 0.25.1", - "jsonrpsee-core 0.25.1", - "jsonrpsee-types 0.25.1", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", "tower", - "url", ] [[package]] @@ -3697,9 +3525,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" dependencies = [ "http", - "jsonrpsee-client-transport 0.26.0", - "jsonrpsee-core 0.26.0", - "jsonrpsee-types 0.26.0", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", "tower", "url", ] @@ -3734,9 +3562,9 @@ dependencies = [ [[package]] name = "l2l-openapi" version = "0.1.0" -source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=5f8fb2237c95725731d5fb10542098303d82e215#5f8fb2237c95725731d5fb10542098303d82e215" +source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=caa8c1c248aca1827b57007dcfcd8ff70d607e8e#caa8c1c248aca1827b57007dcfcd8ff70d607e8e" dependencies = [ - "jsonrpsee 0.25.1", + "jsonrpsee", "l2l-openapi-macros", "utoipa", ] @@ -3744,7 +3572,7 @@ dependencies = [ [[package]] name = "l2l-openapi-macros" version = "0.1.0" -source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=5f8fb2237c95725731d5fb10542098303d82e215#5f8fb2237c95725731d5fb10542098303d82e215" +source = "git+https://github.com/Ash-L2L/l2l-openapi?rev=caa8c1c248aca1827b57007dcfcd8ff70d607e8e#caa8c1c248aca1827b57007dcfcd8ff70d607e8e" dependencies = [ "proc-macro2", "proc_macro_roids", @@ -6595,7 +6423,7 @@ dependencies = [ "governor", "hashlink 0.12.1", "heed", - "jsonrpsee 0.25.1", + "jsonrpsee", "nonempty 0.11.0", "parking_lot", "prost", @@ -6639,7 +6467,7 @@ dependencies = [ "http", "human-size", "include_path", - "jsonrpsee 0.25.1", + "jsonrpsee", "mimalloc", "parking_lot", "poll-promise", @@ -6674,7 +6502,7 @@ dependencies = [ "bitcoin", "clap", "http", - "jsonrpsee 0.25.1", + "jsonrpsee", "serde", "serde_json", "serde_path_to_error", @@ -6693,7 +6521,7 @@ name = "thunder_app_rpc_api" version = "0.17.0" dependencies = [ "bitcoin", - "jsonrpsee 0.25.1", + "jsonrpsee", "l2l-openapi", "serde", "serde_json", @@ -6712,7 +6540,7 @@ dependencies = [ "clap", "dotenvy", "futures", - "jsonrpsee 0.25.1", + "jsonrpsee", "libtest-mimic", "reserve-port", "thiserror 2.0.19", diff --git a/Cargo.toml b/Cargo.toml index 65839c73..fac4181d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ heed = "0.22.0" http = "1.2.0" human-size = "0.4.3" include_path = "0.1.1" -jsonrpsee = { version = "0.25.1", features = ["tracing"] } +jsonrpsee = { version = "0.26.0", features = ["tracing"] } libtest-mimic = "0.8.1" merkle-cbt = "0.3.2" mimalloc = "0.1.48" @@ -95,7 +95,7 @@ path = "bip300301_enforcer/integration_tests" [workspace.dependencies.l2l-openapi] git = "https://github.com/Ash-L2L/l2l-openapi" -rev = "5f8fb2237c95725731d5fb10542098303d82e215" +rev = "caa8c1c248aca1827b57007dcfcd8ff70d607e8e" [workspace.dependencies.rustreexo] git = "https://github.com/mit-dci/rustreexo" From e8d8ffdcccfd1220412753e4adbd9fd0609a999b Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 28 Jul 2026 15:06:33 +0800 Subject: [PATCH 12/18] node: refactor error types, remove dependency on anyhow --- lib/Cargo.toml | 2 +- lib/net/mod.rs | 3 +- lib/net/peer/mailbox.rs | 8 +- lib/net/peer/task.rs | 14 ++-- lib/node/error.rs | 157 +++++++++++++++++++++++++++++++++++++ lib/node/mainchain_task.rs | 27 +++---- lib/node/mod.rs | 80 ++----------------- lib/node/net_task.rs | 111 +++++++++----------------- 8 files changed, 227 insertions(+), 175 deletions(-) create mode 100644 lib/node/error.rs diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a3af33de..c2adb5f3 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -13,7 +13,6 @@ protox = { workspace = true } tonic-prost-build = { workspace = true } [dependencies] -anyhow = { workspace = true } async-lock = { workspace = true } bincode = { workspace = true } bitcoin = { workspace = true } @@ -55,6 +54,7 @@ tracing = { workspace = true } transitive = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } hashlink = { workspace = true } temp-dir = { workspace = true } diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 93d1e5f8..49593159 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -23,6 +23,7 @@ use crate::{ AuthorizedTransaction, Network, THIS_SIDECHAIN, VERSION, Version, net::{Peer, PeerConnectionStatus}, }, + util::ErrorChain, }; pub mod error; @@ -497,7 +498,7 @@ impl Net { let active_peers_read = self.active_peers.read(); let Some(peer_connection_handle) = active_peers_read.get(&addr) else { let err = Error::MissingPeerConnection(addr); - tracing::warn!("{:#}", anyhow::Error::from(err)); + tracing::warn!("{:#}", ErrorChain::new(&err)); return false; }; diff --git a/lib/net/peer/mailbox.rs b/lib/net/peer/mailbox.rs index 3eff4267..d5cf32c4 100644 --- a/lib/net/peer/mailbox.rs +++ b/lib/net/peer/mailbox.rs @@ -28,6 +28,10 @@ use crate::{ util::join_set, }; +type BmmVerificationError = crate::node::error::mainchain_task::Response; + +type MainchainAncestorsError = crate::node::error::net_task::MainchainAncestors; + /// Message received from the connection task / net task / node #[derive(Debug)] pub enum InternalMessage { @@ -38,13 +42,13 @@ pub enum InternalMessage { peer_state_id: PeerStateId, }, /// Indicates an error attempting BMM verification - BmmVerificationError(anyhow::Error), + BmmVerificationError(BmmVerificationError), /// Forward a request ForwardRequest(Request), /// Indicates that mainchain ancestors are now available MainchainAncestors(PeerStateId), /// Indicates an error fetching mainchain ancestors - MainchainAncestorsError(anyhow::Error), + MainchainAncestorsError(MainchainAncestorsError), /// Indicates that the requested headers are now available Headers(PeerStateId), /// Indicates that all requested missing block bodies are now available diff --git a/lib/net/peer/task.rs b/lib/net/peer/task.rs index 0d603ae1..07fc9674 100644 --- a/lib/net/peer/task.rs +++ b/lib/net/peer/task.rs @@ -26,7 +26,7 @@ use crate::{ types::{ AuthorizedTransaction, BlockHash, BmmResult, Header, Tip, VERSION, }, - util::join_set, + util::{ErrorChain, join_set}, }; pub(in crate::net::peer) struct ConnectionTask { @@ -789,12 +789,16 @@ impl ConnectionTask { .map_err(|_| Error::SendBlockingTask)?; } InternalMessage::BmmVerificationError(err) => { - let err: anyhow::Error = err; - tracing::error!("Error attempting BMM verification: {err:#}"); + tracing::error!( + "Error attempting BMM verification: {:#}", + ErrorChain::new(&err) + ); } InternalMessage::MainchainAncestorsError(err) => { - let err: anyhow::Error = err; - tracing::error!("Error fetching mainchain ancestors: {err:#}"); + tracing::error!( + "Error fetching mainchain ancestors: {:#}", + ErrorChain::new(&err) + ); } InternalMessage::MainchainAncestors(peer_state_id) | InternalMessage::Headers(peer_state_id) diff --git a/lib/node/error.rs b/lib/node/error.rs new file mode 100644 index 00000000..180f5894 --- /dev/null +++ b/lib/node/error.rs @@ -0,0 +1,157 @@ +use sneed::{DbError, RwTxnError, env::error as env}; +use thiserror::Error; +use transitive::Transitive; + +use crate::{ + archive, mempool, net, state, + types::{AmountOverflowError, AmountUnderflowError, proto}, +}; + +pub mod mainchain_task { + use sneed::EnvError; + use thiserror::Error; + + use crate::{archive, types::proto}; + + /// Error included in a response + #[derive(Debug, Error)] + pub enum Response { + #[error("Archive error")] + Archive(#[from] archive::Error), + #[error("Database env error")] + DbEnv(#[from] EnvError), + #[error("Database write error")] + DbWrite(#[from] sneed::rwtxn::Error), + #[error("CUSF Mainchain proto error")] + Mainchain(#[from] proto::Error), + } +} + +pub mod net_task { + use std::sync::Arc; + + use futures::channel::{mpsc, oneshot}; + use sneed::{db::error as db, env::error as env, rwtxn::error as rwtxn}; + use thiserror::Error; + use transitive::Transitive; + + use crate::{ + archive, mempool, net, node::error::mainchain_task, state, types::proto, + }; + + #[derive(Debug, Error)] + pub enum MainchainAncestors { + #[error("Requested block was not available: {block_hash}")] + BlockNotAvailable { block_hash: bitcoin::BlockHash }, + #[error(transparent)] + MainchainTaskResponse(#[from] Arc), + } + + #[allow(clippy::duplicated_attributes)] + #[derive(Debug, Error, Transitive)] + #[transitive( + from(db::IterInit, db::Error), + from(db::IterItem, db::Error), + from(env::WriteTxn, env::Error), + from(rwtxn::Commit, rwtxn::Error) + )] + pub enum Error { + #[error("archive error")] + Archive(#[from] archive::Error), + #[error("CUSF mainchain proto error")] + CusfMainchain(#[from] proto::Error), + #[error(transparent)] + Db(#[from] db::Error), + #[error("Database env error")] + DbEnv(#[from] env::Error), + #[error("Database write error")] + DbWrite(#[from] rwtxn::Error), + #[error("Forward mainchain task request failed")] + ForwardMainchainTaskRequest, + #[error("mempool error")] + MemPool(#[from] mempool::Error), + #[error("Net error")] + Net(#[from] Box), + #[error("peer info stream closed")] + PeerInfoRxClosed, + #[error("Receive mainchain task response cancelled")] + ReceiveMainchainTaskResponse, + #[error("Receive reorg result cancelled (oneshot)")] + ReceiveReorgResultOneshot(#[source] oneshot::Canceled), + #[error("Send mainchain task request failed")] + SendMainchainTaskRequest, + #[error("Send new tip ready failed")] + SendNewTipReady(#[source] mpsc::SendError), + #[error("Send reorg result error (oneshot)")] + SendReorgResultOneshot, + #[error("state error")] + State(#[from] state::Error), + } + + impl From for Error { + fn from(err: net::Error) -> Self { + Self::Net(Box::new(err)) + } + } +} + +#[derive(Debug, Error, Transitive)] +#[transitive(from(env::ReadTxn, env::Error))] +pub enum Error { + #[error("address parse error")] + AddrParse(#[from] std::net::AddrParseError), + #[error(transparent)] + AmountOverflow(#[from] AmountOverflowError), + #[error(transparent)] + AmountUnderflow(#[from] AmountUnderflowError), + #[error("archive error")] + Archive(#[from] archive::Error), + #[error("CUSF mainchain proto error")] + CusfMainchain(#[from] proto::Error), + #[error(transparent)] + Db(#[from] DbError), + #[error("Database env error")] + DbEnv(#[from] env::Error), + #[error("Database write error")] + DbWrite(#[from] RwTxnError), + #[error("I/O error")] + Io(#[from] std::io::Error), + #[error("error requesting mainchain ancestors")] + MainchainAncestors(#[source] mainchain_task::Response), + #[error("mempool error")] + MemPool(#[from] mempool::Error), + #[error("net error")] + Net(#[from] Box), + #[error("net task error")] + NetTask(#[source] Box), + #[error("No CUSF mainchain wallet client")] + NoCusfMainchainWalletClient, + #[error("peer info stream closed")] + PeerInfoRxClosed, + #[error("Receive mainchain task response cancelled")] + ReceiveMainchainTaskResponse, + #[error("Send mainchain task request failed")] + SendMainchainTaskRequest, + #[error("state error")] + State(#[source] Box), + #[error("Utreexo error: {0}")] + Utreexo(String), +} + +impl From for Error { + fn from(err: net::Error) -> Self { + Self::Net(Box::new(err)) + } +} + +impl From for Error { + fn from(err: net_task::Error) -> Self { + Self::NetTask(Box::new(err)) + } +} + +impl From for Error { + fn from(err: state::Error) -> Self { + Self::State(Box::new(err)) + } +} diff --git a/lib/node/mainchain_task.rs b/lib/node/mainchain_task.rs index 92301ac6..25551509 100644 --- a/lib/node/mainchain_task.rs +++ b/lib/node/mainchain_task.rs @@ -21,8 +21,10 @@ use tokio::{ }; use crate::{ - archive::{self, Archive}, + archive::Archive, + node::error::mainchain_task as error, types::proto::{self, mainchain}, + util::ErrorChain, }; /// Request data from the mainchain node @@ -32,24 +34,11 @@ pub(super) enum Request { AncestorInfos(bitcoin::BlockHash), } -/// Error included in a response -#[derive(Debug, Error)] -pub enum ResponseError { - #[error("Archive error")] - Archive(#[from] archive::Error), - #[error("Database env error")] - DbEnv(#[from] EnvError), - #[error("Database write error")] - DbWrite(#[from] sneed::rwtxn::Error), - #[error("CUSF Mainchain proto error")] - Mainchain(#[from] proto::Error), -} - /// Response indicating that a request has been fulfilled #[derive(Debug)] pub(super) enum Response { /// Response bool indicates if the requested header was available - AncestorInfos(bitcoin::BlockHash, Result), + AncestorInfos(bitcoin::BlockHash, Result), } impl From<&Response> for Request { @@ -92,7 +81,7 @@ where archive: &Archive, cusf_mainchain: &mut proto::mainchain::ValidatorClient, block_hash: bitcoin::BlockHash, - ) -> Result { + ) -> Result { if block_hash == bitcoin::BlockHash::all_zeros() { return Ok(true); } else { @@ -233,8 +222,10 @@ impl MainchainTaskHandle { }; let task = spawn(async move { if let Err(err) = task.run().await { - let err = anyhow::Error::from(err); - tracing::error!("Mainchain task error: {err:#}"); + tracing::error!( + "Mainchain task error: {:#}", + ErrorChain::new(&err) + ); } }); let task_handle = MainchainTaskHandle { diff --git a/lib/node/mod.rs b/lib/node/mod.rs index f0f39ace..dae4c4af 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -1,7 +1,6 @@ use std::{ borrow::BorrowMut, collections::{HashMap, HashSet}, - fmt::Debug, net::SocketAddr, path::Path, sync::Arc, @@ -10,15 +9,15 @@ use std::{ use bitcoin::amount::CheckedSum; use fallible_iterator::{FallibleIterator, IteratorExt}; use futures::{Stream, future::BoxFuture}; -use sneed::{DbError, Env, EnvError, RwTxnError, env}; +use sneed::{DbError, Env, EnvError, RwTxnError}; use tokio::sync::Mutex; use tonic::transport::Channel; use crate::{ - archive::{self, Archive}, + archive::Archive, mempool::{self, MemPool}, - net::{self, Net}, - state::{self, State}, + net::Net, + state::State, types::{ Accumulator, Address, AmountOverflowError, AmountUnderflowError, Authorized, AuthorizedTransaction, BlockHash, BmmResult, Body, @@ -30,75 +29,12 @@ use crate::{ util::Watchable, }; +pub(crate) mod error; +pub use error::Error; mod mainchain_task; -mod net_task; - use mainchain_task::MainchainTaskHandle; - -use self::net_task::NetTaskHandle; - -#[derive(Debug, thiserror::Error, transitive::Transitive)] -#[transitive(from(env::error::ReadTxn, EnvError))] -pub enum Error { - #[error("address parse error")] - AddrParse(#[from] std::net::AddrParseError), - #[error(transparent)] - AmountOverflow(#[from] AmountOverflowError), - #[error(transparent)] - AmountUnderflow(#[from] AmountUnderflowError), - #[error("archive error")] - Archive(#[from] archive::Error), - #[error("CUSF mainchain proto error")] - CusfMainchain(#[from] proto::Error), - #[error(transparent)] - Db(#[from] DbError), - #[error("Database env error")] - DbEnv(#[from] EnvError), - #[error("Database write error")] - DbWrite(#[from] RwTxnError), - #[error("I/O error")] - Io(#[from] std::io::Error), - #[error("error requesting mainchain ancestors")] - MainchainAncestors(#[source] mainchain_task::ResponseError), - #[error("mempool error")] - MemPool(#[from] mempool::Error), - #[error("net error")] - Net(#[from] Box), - #[error("net task error")] - NetTask(#[source] Box), - #[error("No CUSF mainchain wallet client")] - NoCusfMainchainWalletClient, - #[error("peer info stream closed")] - PeerInfoRxClosed, - #[error("Receive mainchain task response cancelled")] - ReceiveMainchainTaskResponse, - #[error("Send mainchain task request failed")] - SendMainchainTaskRequest, - #[error("state error")] - State(#[source] Box), - #[error("Utreexo error: {0}")] - Utreexo(String), - #[error("Verify BMM error")] - VerifyBmm(anyhow::Error), -} - -impl From for Error { - fn from(err: net::Error) -> Self { - Self::Net(Box::new(err)) - } -} - -impl From for Error { - fn from(err: net_task::Error) -> Self { - Self::NetTask(Box::new(err)) - } -} - -impl From for Error { - fn from(err: state::Error) -> Self { - Self::State(Box::new(err)) - } -} +mod net_task; +use net_task::NetTaskHandle; #[derive(Clone)] pub struct Node { diff --git a/lib/node/net_task.rs b/lib/node/net_task.rs index ff7013d9..5ef4deb4 100644 --- a/lib/node/net_task.rs +++ b/lib/node/net_task.rs @@ -13,84 +13,33 @@ use fallible_iterator::{FallibleIterator, IteratorExt}; use futures::{ StreamExt, channel::{ - mpsc::{self, TrySendError, UnboundedReceiver, UnboundedSender}, + mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot, }, stream, }; use nonempty::NonEmpty; -use sneed::{ - DbError, EnvError, RwTxn, RwTxnError, db, env::error as env_error, - rwtxn::error as rwtxn_error, -}; -use thiserror::Error; +use sneed::{DbError, EnvError, RwTxn, RwTxnError}; use tokio::task::{self, JoinHandle}; use tokio_stream::StreamNotifyClose; -use super::mainchain_task::{self, MainchainTaskHandle}; use crate::{ - archive::{self, Archive}, - mempool::{self, MemPool}, + archive::Archive, + mempool::MemPool, net::{ self, Net, PeerConnectionError, PeerConnectionInfo, PeerConnectionMailboxError, PeerConnectionMessage, PeerInfoRx, PeerRequest, PeerResponse, PeerStateId, peer_message, }, - state::{self, State}, - types::{ - BmmResult, Body, Header, MerkleRoot, Tip, - proto::{self, mainchain}, + node::{ + error::net_task::{self as error, Error}, + mainchain_task::{self, MainchainTaskHandle}, }, + state::{self, State}, + types::{BmmResult, Body, Header, MerkleRoot, Tip, proto::mainchain}, util::{ErrorChain, join_set}, }; -#[allow(clippy::duplicated_attributes)] -#[derive(transitive::Transitive, Debug, Error)] -#[transitive( - from(db::error::IterInit, DbError), - from(db::error::IterItem, DbError), - from(env_error::WriteTxn, EnvError), - from(rwtxn_error::Commit, RwTxnError) -)] -pub enum Error { - #[error("archive error")] - Archive(#[from] archive::Error), - #[error("CUSF mainchain proto error")] - CusfMainchain(#[from] proto::Error), - #[error(transparent)] - Db(#[from] DbError), - #[error("Database env error")] - DbEnv(#[from] EnvError), - #[error("Database write error")] - DbWrite(#[from] RwTxnError), - #[error("Forward mainchain task request failed")] - ForwardMainchainTaskRequest, - #[error("mempool error")] - MemPool(#[from] mempool::Error), - #[error("Net error")] - Net(#[from] Box), - #[error("peer info stream closed")] - PeerInfoRxClosed, - #[error("Receive mainchain task response cancelled")] - ReceiveMainchainTaskResponse, - #[error("Receive reorg result cancelled (oneshot)")] - ReceiveReorgResultOneshot(#[source] oneshot::Canceled), - #[error("Send mainchain task request failed")] - SendMainchainTaskRequest, - #[error("Send new tip ready failed")] - SendNewTipReady(#[source] TrySendError), - #[error("Send reorg result error (oneshot)")] - SendReorgResultOneshot, - #[error("state error")] - State(#[from] state::Error), -} - -impl From for Error { - fn from(err: net::Error) -> Self { - Self::Net(Box::new(err)) - } -} - fn connect_tip_( rwtxn: &mut RwTxn<'_>, archive: &Archive, @@ -626,7 +575,9 @@ impl NetTask { let () = new_tip_ready_tx .unbounded_send((block_tip, Some(addr), None)) - .map_err(Error::SendNewTipReady)?; + .map_err(|err| { + Error::SendNewTipReady(err.into_send_error()) + })?; } let Some(block_descendant_tips) = descendant_tips.remove(&block_hash) @@ -711,7 +662,11 @@ impl NetTask { Some(addr), None, )) - .map_err(Error::SendNewTipReady)?; + .map_err(|err| { + Error::SendNewTipReady( + err.into_send_error(), + ) + })?; } } } @@ -908,9 +863,9 @@ impl NetTask { let non_fatal_err: ::Jfyi = non_fatal_err; - let non_fatal_err = anyhow::Error::from(non_fatal_err); tracing::error!( - "Failed to accept connection: {non_fatal_err:#}" + "Failed to accept connection: {:#}", + ErrorChain::new(&non_fatal_err) ); None } @@ -975,9 +930,9 @@ impl NetTask { // explicitly type error let fatal_err: ::Fatal = fatal_err; - let fatal_err = anyhow::Error::from(fatal_err); tracing::error!( - "failed to accept connection: {fatal_err:#}" + "failed to accept connection: {:#}", + ErrorChain::new(&fatal_err) ); } }, @@ -1015,10 +970,10 @@ impl NetTask { peer_state_id, ), Ok(false) => PeerConnectionMessage::MainchainAncestorsError( - anyhow::anyhow!("Requested block was not available: {block_hash}") + error::MainchainAncestors::BlockNotAvailable { block_hash } ), Err(ref err) => PeerConnectionMessage::MainchainAncestorsError( - anyhow::Error::from(err.clone()) + err.clone().into() ) }; let _: bool = self @@ -1106,8 +1061,9 @@ impl NetTask { }); } PeerConnectionInfo::Error(err) => { - let err = anyhow::anyhow!(err); - tracing::error!(%addr, err = format!("{err:#}"), "Peer connection error"); + let err_msg = + format!("{:#}", ErrorChain::new(&err)); + tracing::error!(%addr, err = err_msg, "Peer connection error"); let () = self.ctxt.net.remove_active_peer(addr); } PeerConnectionInfo::NeedMainchainAncestors { @@ -1133,7 +1089,11 @@ impl NetTask { ); self.new_tip_ready_tx .unbounded_send((new_tip, Some(addr), None)) - .map_err(Error::SendNewTipReady)?; + .map_err(|err| { + Error::SendNewTipReady( + err.into_send_error(), + ) + })?; } PeerConnectionInfo::NewTransaction(mut new_tx) => { let mut rwtxn = self @@ -1181,10 +1141,10 @@ impl NetTask { { Ok(()) => (), Err(err) => { - let err = anyhow::Error::from(err); tracing::error!( %peer_address, - "Failed to connect to peer: {err:#}" + "Failed to connect to peer: {:#}", + ErrorChain::new(&err) ) } } @@ -1246,8 +1206,7 @@ impl NetTaskHandle { }; let task = runtime.spawn(async { if let Err(err) = task.run().await { - let err = anyhow::Error::from(err); - tracing::error!("Net task error: {err:#}"); + tracing::error!("Net task error: {:#}", ErrorChain::new(&err)); } }); NetTaskHandle { @@ -1270,7 +1229,7 @@ impl NetTaskHandle { let () = self .new_tip_ready_tx .unbounded_send((new_tip, None, Some(oneshot_tx))) - .map_err(Error::SendNewTipReady)?; + .map_err(|err| Error::SendNewTipReady(err.into_send_error()))?; oneshot_rx.await.map_err(Error::ReceiveReorgResultOneshot) } } From a096670c60108d43ff68bbe6fd22408281467200 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 4 Aug 2026 17:50:07 +0800 Subject: [PATCH 13/18] CLI: add option to override network magic --- Cargo.lock | 1 + app/Cargo.toml | 1 + app/app.rs | 1 + app/cli.rs | 10 ++++++++++ lib/net/mod.rs | 13 ++++++++----- lib/net/peer/mod.rs | 42 ++++++++++++++++++++++-------------------- lib/net/peer/task.rs | 12 ++++++------ lib/node/mod.rs | 11 +++++++++-- 8 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ffd3c0e..6ea1daea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6460,6 +6460,7 @@ dependencies = [ "bincode", "bitcoin", "clap", + "const-hex", "dirs", "eframe", "fallible-iterator", diff --git a/app/Cargo.toml b/app/Cargo.toml index 52c83c62..45a14668 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -11,6 +11,7 @@ anyhow = { workspace = true } bincode = { workspace = true } bitcoin = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } +const-hex = { workspace = true } dirs = { workspace = true } fallible-iterator = { workspace = true } futures = { workspace = true } diff --git a/app/app.rs b/app/app.rs index 54f8dfa9..57dc01a7 100644 --- a/app/app.rs +++ b/app/app.rs @@ -271,6 +271,7 @@ impl App { config.net_addr, cusf_mainchain, cusf_mainchain_wallet, + config.network_magic_override, config.network, &runtime, )?; diff --git a/app/cli.rs b/app/cli.rs index 8c09dec2..d8112289 100644 --- a/app/cli.rs +++ b/app/cli.rs @@ -95,6 +95,11 @@ impl clap::Args for DatadirArg { } } +#[inline(always)] +fn parse_network_magic(s: &str) -> Result<[u8; 4], const_hex::FromHexError> { + const_hex::decode_to_array(s) +} + #[derive(Clone, Debug, Parser)] #[command(author, version, about, long_about = None)] pub(super) struct Cli { @@ -133,6 +138,9 @@ pub(super) struct Cli { /// Set the network. Setting this may affect other defaults. #[arg(default_value_t, long, value_enum)] network: Network, + /// Manually provide the network magic bytes + #[arg(long, value_parser = parse_network_magic)] + network_magic: Option<[u8; 4]>, /// Socket address to host the RPC server #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] rpc_addr: SocketAddr, @@ -150,6 +158,7 @@ pub struct Config { pub mnemonic_seed_phrase_path: Option, pub net_addr: SocketAddr, pub network: Network, + pub network_magic_override: Option, pub rpc_addr: SocketAddr, } @@ -186,6 +195,7 @@ impl Cli { mnemonic_seed_phrase_path: self.mnemonic_seed_phrase_path, net_addr: self.net_addr, network: self.network, + network_magic_override: self.network_magic, rpc_addr: self.rpc_addr, }) } diff --git a/lib/net/mod.rs b/lib/net/mod.rs index 49593159..328fdb00 100644 --- a/lib/net/mod.rs +++ b/lib/net/mod.rs @@ -202,7 +202,7 @@ const fn seed_node_addrs(network: Network) -> &'static [SocketAddr] { pub struct Net { pub server: Endpoint, archive: Archive, - network: Network, + magic_bytes: peer_message::MagicBytes, state: State, active_peers: Arc>>, // None indicates that the stream has ended @@ -297,7 +297,7 @@ impl Net { let connection_ctxt = PeerConnectionCtxt { env, archive: self.archive.clone(), - network: self.network, + magic_bytes: self.magic_bytes, state: self.state.clone(), }; @@ -335,6 +335,7 @@ impl Net { pub fn new( env: &sneed::Env, archive: Archive, + magic_bytes_override: Option, network: Network, state: State, bind_addr: SocketAddr, @@ -359,11 +360,13 @@ impl Net { version.put(&mut rwtxn, &(), &*VERSION)?; } rwtxn.commit().map_err(RwTxnError::from)?; + let magic_bytes = magic_bytes_override + .unwrap_or_else(|| peer_message::magic_bytes(network)); let (peer_info_tx, peer_info_rx) = mpsc::unbounded(); let net = Net { server, archive, - network, + magic_bytes, state, active_peers, peer_info_tx, @@ -436,7 +439,7 @@ impl Net { remote_address, } })?; - Connection::new(raw_conn, self.network) + Connection::new(raw_conn, self.magic_bytes) } None => { tracing::debug!("server endpoint closed"); @@ -468,7 +471,7 @@ impl Net { let connection_ctxt = PeerConnectionCtxt { env, archive: self.archive.clone(), - network: self.network, + magic_bytes: self.magic_bytes, state: self.state.clone(), }; let (connection_handle, info_rx) = diff --git a/lib/net/peer/mod.rs b/lib/net/peer/mod.rs index cb49641f..96e93939 100644 --- a/lib/net/peer/mod.rs +++ b/lib/net/peer/mod.rs @@ -19,7 +19,7 @@ use crate::{ archive::Archive, state::State, types::{ - AuthorizedTransaction, Hash, Network, Tip, Version, hash, + AuthorizedTransaction, Hash, Tip, Version, hash, net::PeerConnectionStatus, }, }; @@ -143,7 +143,7 @@ where #[derive(Clone)] pub struct Connection { pub(in crate::net) inner: quinn::Connection, - pub network: Network, + pub magic_bytes: message::MagicBytes, } impl Connection { @@ -182,16 +182,19 @@ impl Connection { Self::MIN_READ_RESPONSE_TIMEOUT.saturating_add(body_allowance) } - pub fn new(connection: quinn::Connection, network: Network) -> Self { + pub fn new( + connection: quinn::Connection, + magic_bytes: message::MagicBytes, + ) -> Self { Self { inner: connection, - network, + magic_bytes, } } pub async fn from_connecting( connecting: quinn::Connecting, - network: Network, + magic_bytes: message::MagicBytes, ) -> Result { let addr = connecting.remote_address(); tracing::trace!(%addr, "connecting to peer"); @@ -199,7 +202,7 @@ impl Connection { tracing::info!(%addr, "connected successfully to peer"); Ok(Self { inner: connection, - network, + magic_bytes, }) } @@ -213,7 +216,7 @@ impl Connection { rx.read_exact(&mut magic_bytes) .await .map_err(error::connection::Receive::ReadMagic)?; - if magic_bytes != message::magic_bytes(self.network) { + if magic_bytes != self.magic_bytes { return Err( error::connection::Receive::BadMagic(magic_bytes).into() ); @@ -239,7 +242,7 @@ impl Connection { "Sending heartbeat" ); let message = RequestMessageRef::from(heartbeat); - let mut message_buf = message::magic_bytes(self.network).to_vec(); + let mut message_buf = self.magic_bytes.to_vec(); bincode::serialize_into::<&mut Vec<_>, _>(&mut message_buf, &message)?; send.write_all(&message_buf).await.map_err(|err| { error::connection::Send::Write { @@ -252,7 +255,7 @@ impl Connection { } async fn receive_response( - network: Network, + expected_magic_bytes: message::MagicBytes, mut recv: RecvStream, read_response_limit: NonZeroUsize, ) -> ResponseResult { @@ -261,7 +264,7 @@ impl Connection { recv.read_exact(&mut magic_bytes) .await .map_err(error::connection::Receive::ReadMagic)?; - if magic_bytes != message::magic_bytes(network) { + if magic_bytes != expected_magic_bytes { return Err( error::connection::Receive::BadMagic(magic_bytes).into() ); @@ -289,7 +292,7 @@ impl Connection { "Sending request" ); let message = RequestMessageRef::from(request); - let mut message_buf = message::magic_bytes(self.network).to_vec(); + let mut message_buf = self.magic_bytes.to_vec(); bincode::serialize_into::<&mut Vec<_>, _>(&mut message_buf, &message)?; send.write_all(&message_buf).await.map_err(|err| { error::connection::Send::Write { @@ -304,7 +307,7 @@ impl Connection { // 10MB) block response on a slow or congested link is not aborted. let response = match tokio::time::timeout( Self::response_read_timeout(read_response_limit), - Self::receive_response(self.network, recv, read_response_limit), + Self::receive_response(self.magic_bytes, recv, read_response_limit), ) .await { @@ -317,7 +320,7 @@ impl Connection { // Send a pre-serialized response, where the response does not include // magic bytes async fn send_serialized_response( - network: Network, + magic_bytes: message::MagicBytes, mut response_tx: SendStream, serialized_response: &[u8], ) -> Result<(), error::connection::SendResponse> { @@ -326,9 +329,7 @@ impl Connection { "Sending response" ); async { - response_tx - .write_all(&message::magic_bytes(network)) - .await?; + response_tx.write_all(&magic_bytes).await?; response_tx.write_all(serialized_response).await } .await @@ -344,7 +345,7 @@ impl Connection { } async fn send_response( - network: Network, + magic_bytes: message::MagicBytes, mut response_tx: SendStream, response: ResponseMessage, ) -> Result<(), error::connection::SendResponse> { @@ -353,7 +354,7 @@ impl Connection { send_id = %response_tx.id(), "Sending response" ); - let mut message_buf = message::magic_bytes(network).to_vec(); + let mut message_buf = magic_bytes.to_vec(); bincode::serialize_into::<&mut Vec<_>, _>(&mut message_buf, &response)?; response_tx.write_all(&message_buf).await.map_err(|err| { { @@ -370,7 +371,7 @@ impl Connection { pub struct ConnectionContext { pub env: sneed::Env, pub archive: Archive, - pub network: Network, + pub magic_bytes: message::MagicBytes, pub state: State, } @@ -520,7 +521,8 @@ pub fn connect( let info_tx = info_tx.clone(); move || async move { let connection = - Connection::from_connecting(connecting, ctxt.network).await?; + Connection::from_connecting(connecting, ctxt.magic_bytes) + .await?; status.store( PeerConnectionStatus::Connected.into(), atomic::Ordering::SeqCst, diff --git a/lib/net/peer/task.rs b/lib/net/peer/task.rs index 07fc9674..af9534d7 100644 --- a/lib/net/peer/task.rs +++ b/lib/net/peer/task.rs @@ -609,8 +609,8 @@ impl ConnectionTask { } (_, _) => ResponseMessage::NoBlock { block_hash }, }; - let () = - Connection::send_response(ctxt.network, response_tx, resp).await?; + let () = Connection::send_response(ctxt.magic_bytes, response_tx, resp) + .await?; Ok(()) } @@ -662,7 +662,7 @@ impl ConnectionTask { match validate_tx_result { Err(err) => { Connection::send_response( - ctxt.network, + ctxt.magic_bytes, response_tx, ResponseMessage::TransactionRejected(txid), ) @@ -671,7 +671,7 @@ impl ConnectionTask { } Ok(_) => { Connection::send_response( - ctxt.network, + ctxt.magic_bytes, response_tx, ResponseMessage::TransactionAccepted(txid), ) @@ -851,10 +851,10 @@ impl ConnectionTask { serialized_response, response_tx, }) => { - let network = ctxt.network; + let magic_bytes = ctxt.magic_bytes; self.mailbox_tx.send_response_spawner.spawn(async move { Connection::send_serialized_response( - network, + magic_bytes, response_tx, &serialized_response, ) diff --git a/lib/node/mod.rs b/lib/node/mod.rs index dae4c4af..83466efb 100644 --- a/lib/node/mod.rs +++ b/lib/node/mod.rs @@ -61,6 +61,7 @@ where cusf_mainchain_wallet: Option< mainchain::WalletClient, >, + magic_bytes_override: Option, network: Network, runtime: &tokio::runtime::Runtime, ) -> Result @@ -119,8 +120,14 @@ where archive.clone(), cusf_mainchain.clone(), ); - let (net, peer_info_rx) = - Net::new(&env, archive.clone(), network, state.clone(), bind_addr)?; + let (net, peer_info_rx) = Net::new( + &env, + archive.clone(), + magic_bytes_override, + network, + state.clone(), + bind_addr, + )?; let net_task = NetTaskHandle::new( runtime, From 75417017f7c67d7104c7c68b3d89b0b622096106 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Tue, 4 Aug 2026 18:06:11 +0800 Subject: [PATCH 14/18] Bump version for release 0.17.1 --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ea1daea..766a5f13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6404,7 +6404,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "async-lock", @@ -6454,7 +6454,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "bincode", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "thunder_app_cli" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "bitcoin", @@ -6519,7 +6519,7 @@ dependencies = [ [[package]] name = "thunder_app_rpc_api" -version = "0.17.0" +version = "0.17.1" dependencies = [ "bitcoin", "jsonrpsee", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "thunder_integration_tests" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "bip300301_enforcer_integration_tests", @@ -6555,7 +6555,7 @@ dependencies = [ [[package]] name = "thunder_types" -version = "0.17.0" +version = "0.17.1" dependencies = [ "anyhow", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index fac4181d..5dd6977c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ edition = "2024" license-file = "LICENSE.txt" publish = false -version = "0.17.0" +version = "0.17.1" [workspace.dependencies] anyhow = "1.0.72" From b7709e055027b6d2f7ea9fe200b8bb832bebb923 Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Thu, 6 Aug 2026 22:37:27 +0800 Subject: [PATCH 15/18] RPC server: add option to run private APIs on another socket address --- app/cli.rs | 5 + app/main.rs | 8 +- app/rpc_server.rs | 510 ++++++++++++++---------- cli/lib.rs | 14 +- integration_tests/ibd.rs | 2 +- integration_tests/integration_test.rs | 2 +- integration_tests/setup.rs | 2 +- integration_tests/unknown_withdrawal.rs | 2 +- integration_tests/util.rs | 2 + rpc-api/lib.rs | 506 ++++++++++++----------- 10 files changed, 613 insertions(+), 440 deletions(-) diff --git a/app/cli.rs b/app/cli.rs index d8112289..7a443656 100644 --- a/app/cli.rs +++ b/app/cli.rs @@ -141,6 +141,9 @@ pub(super) struct Cli { /// Manually provide the network magic bytes #[arg(long, value_parser = parse_network_magic)] network_magic: Option<[u8; 4]>, + /// Socket address to host the private RPC server + #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] + private_rpc_addr: SocketAddr, /// Socket address to host the RPC server #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] rpc_addr: SocketAddr, @@ -159,6 +162,7 @@ pub struct Config { pub net_addr: SocketAddr, pub network: Network, pub network_magic_override: Option, + pub private_rpc_addr: SocketAddr, pub rpc_addr: SocketAddr, } @@ -196,6 +200,7 @@ impl Cli { net_addr: self.net_addr, network: self.network, network_magic_override: self.network_magic, + private_rpc_addr: self.private_rpc_addr, rpc_addr: self.rpc_addr, }) } diff --git a/app/main.rs b/app/main.rs index 87db568f..2515efd8 100644 --- a/app/main.rs +++ b/app/main.rs @@ -219,8 +219,12 @@ fn main() -> anyhow::Result<()> { let app = app.clone(); async move { tracing::info!("starting RPC server at `{}`", config.rpc_addr); - if let Err(err) = - rpc_server::run_server(app, config.rpc_addr).await + if let Err(err) = rpc_server::run_server( + app, + config.private_rpc_addr, + config.rpc_addr, + ) + .await { app_tx.send(err).expect("failed to send error to app"); } diff --git a/app/rpc_server.rs b/app/rpc_server.rs index ae18c06b..d08eb120 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -10,7 +10,7 @@ use thunder::types::{ Address, Pointed, PointedOutput, SpentOutput, Txid, WithdrawalBundle, net::Peer, wallet::Balance, }; -use thunder_app_rpc_api::{GetTransactionResponse, RpcServer}; +use thunder_app_rpc_api as rpc_api; use tower_http::{ cors::CorsLayer, request_id::{ @@ -21,10 +21,6 @@ use tower_http::{ use crate::app::App; -pub struct RpcServerImpl { - app: App, -} - fn custom_err_msg(err_msg: impl Into) -> ErrorObject<'static> { ErrorObject::owned(-1, err_msg.into(), Option::<()>::None) } @@ -36,91 +32,54 @@ where let error = anyhow::Error::from(error); custom_err_msg(format!("{error:#}")) } -#[async_trait] -impl RpcServer for RpcServerImpl { - async fn balance(&self) -> RpcResult { - self.app.wallet.get_balance().map_err(custom_err) - } - async fn create_deposit( - &self, - address: Address, - value_sats: u64, - fee_sats: u64, - ) -> RpcResult { - let app = self.app.clone(); - tokio::task::spawn_blocking(move || { - app.deposit_blocking( - address, - bitcoin::Amount::from_sat(value_sats), - bitcoin::Amount::from_sat(fee_sats), - ) - .map_err(custom_err) - }) - .await - .unwrap() +#[derive(Clone)] +#[repr(transparent)] +pub struct RpcServerImpl { + app: App, +} + +pub struct PrivateOnlyRpcServerImpl; + +#[async_trait] +impl rpc_api::open_api::RpcServer for PrivateOnlyRpcServerImpl { + async fn openapi_schema(&self) -> RpcResult { + use utoipa::OpenApi as _; + let mut res = rpc_api::open_api::RpcDoc::openapi(); + res.merge(rpc_api::node::PrivateRpcDoc::openapi()); + res.merge(rpc_api::wallet::RpcDoc::openapi()); + Ok(res) } +} - async fn create_transfer( - &self, - dest: Address, - value_sats: u64, - fee_sats: u64, - ) -> RpcResult { - let accumulator = - self.app.node.get_tip_accumulator().map_err(custom_err)?; - let tx = self - .app - .wallet - .create_transaction( - &accumulator, - dest, - Amount::from_sat(value_sats), - Amount::from_sat(fee_sats), - ) - .map_err(custom_err)?; - let txid = tx.txid(); - let () = self.app.sign_and_send(tx).map_err(custom_err)?; - Ok(txid) +#[async_trait] +impl rpc_api::open_api::RpcServer for RpcServerImpl { + async fn openapi_schema(&self) -> RpcResult { + use utoipa::OpenApi as _; + let mut res = rpc_api::open_api::RpcDoc::openapi(); + res.merge(rpc_api::node::RpcDoc::openapi()); + Ok(res) } +} - async fn create_withdrawal( - &self, - mainchain_address: bitcoin::Address, - amount_sats: u64, - fee_sats: u64, - mainchain_fee_sats: u64, - ) -> RpcResult { - let accumulator = - self.app.node.get_tip_accumulator().map_err(custom_err)?; - let tx = self - .app - .wallet - .create_withdrawal( - &accumulator, - mainchain_address, - Amount::from_sat(amount_sats), - Amount::from_sat(mainchain_fee_sats), - Amount::from_sat(fee_sats), - ) - .map_err(custom_err)?; - let txid = tx.txid(); - let () = self.app.sign_and_send(tx).map_err(custom_err)?; - Ok(txid) +#[async_trait] +impl rpc_api::open_api::RpcServer for RpcServerImpl { + async fn openapi_schema(&self) -> RpcResult { + use utoipa::OpenApi as _; + let mut res = rpc_api::open_api::RpcDoc::openapi(); + res.merge(rpc_api::node::PrivateRpcDoc::openapi()); + res.merge(rpc_api::node::RpcDoc::openapi()); + res.merge(rpc_api::wallet::RpcDoc::openapi()); + Ok(res) } +} +#[async_trait] +impl rpc_api::node::PrivateRpcServer for RpcServerImpl { async fn connect_peer(&self, addr: SocketAddr) -> RpcResult<()> { self.app.node.connect_peer(addr).map_err(custom_err) } - async fn format_deposit_address( - &self, - address: Address, - ) -> RpcResult { - let deposit_address = address.format_for_deposit(); - Ok(deposit_address) - } - async fn forget_peer(&self, addr: SocketAddr) -> RpcResult<()> { match self.app.node.forget_peer(&addr) { Ok(_) => Ok(()), @@ -128,14 +87,19 @@ impl RpcServer for RpcServerImpl { } } - async fn generate_mnemonic(&self) -> RpcResult { - let mnemonic = bip39::Mnemonic::new( - bip39::MnemonicType::Words12, - bip39::Language::English, - ); - Ok(mnemonic.to_string()) + async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()> { + self.app.node.remove_from_mempool(txid).map_err(custom_err) + } + + async fn stop(&self) { + std::process::exit(0); } +} +#[async_trait] +impl rpc_api::node::RpcServer + for RpcServerImpl +{ async fn get_block( &self, block_hash: thunder::types::BlockHash, @@ -186,10 +150,6 @@ impl RpcServer for RpcServerImpl { .map_err(custom_err) } - async fn get_new_address(&self) -> RpcResult
{ - self.app.wallet.get_new_address().map_err(custom_err) - } - async fn get_stxos( &self, addresses: HashSet
, @@ -208,13 +168,15 @@ impl RpcServer for RpcServerImpl { async fn get_transaction( &self, txid: Txid, - ) -> RpcResult> { - let res = self - .app - .node - .try_get_transaction(txid) - .map_err(custom_err)? - .map(|(tx, block_hash)| GetTransactionResponse { tx, block_hash }); + ) -> RpcResult> { + let res = + self.app + .node + .try_get_transaction(txid) + .map_err(custom_err)? + .map(|(tx, block_hash)| { + rpc_api::node::GetTransactionResponse { tx, block_hash } + }); Ok(res) } @@ -233,22 +195,6 @@ impl RpcServer for RpcServerImpl { Ok(res) } - async fn get_wallet_addresses(&self) -> RpcResult> { - let addrs = self.app.wallet.get_addresses().map_err(custom_err)?; - let mut res: Vec<_> = addrs.into_iter().collect(); - res.sort_by_key(|addr| addr.as_base58()); - Ok(res) - } - - async fn get_wallet_utxos(&self) -> RpcResult> { - let utxos = self.app.wallet.get_utxos().map_err(custom_err)?; - let utxos = utxos - .into_iter() - .map(|(outpoint, output)| PointedOutput { outpoint, output }) - .collect(); - Ok(utxos) - } - async fn getblockcount(&self) -> RpcResult { let height = self.app.node.try_get_height().map_err(custom_err)?; let block_count = height.map_or(0, |height| height + 1); @@ -280,18 +226,6 @@ impl RpcServer for RpcServerImpl { Ok(res) } - async fn mine(&self, fee: Option) -> RpcResult<()> { - let fee = fee.map(bitcoin::Amount::from_sat); - self.app - .local_pool - .spawn_pinned({ - let app = self.app.clone(); - move || async move { app.mine(fee).await.map_err(custom_err) } - }) - .await - .unwrap() - } - async fn pending_withdrawal_bundle( &self, ) -> RpcResult> { @@ -301,13 +235,143 @@ impl RpcServer for RpcServerImpl { .map_err(custom_err) } - async fn openapi_schema(&self) -> RpcResult { - let res = ::openapi(); + async fn sidechain_wealth_sats(&self) -> RpcResult { + let sidechain_wealth = + self.app.node.get_sidechain_wealth().map_err(custom_err)?; + Ok(sidechain_wealth.to_sat()) + } + + async fn submit_transaction( + &self, + mut transaction: thunder::types::AuthorizedTransaction, + ) -> RpcResult { + let () = self + .app + .submit_transaction(&mut transaction) + .map_err(custom_err)?; + Ok(transaction.transaction.txid()) + } +} + +#[async_trait] +impl rpc_api::wallet::RpcServer for RpcServerImpl { + async fn balance(&self) -> RpcResult { + self.app.wallet.get_balance().map_err(custom_err) + } + + async fn create_deposit( + &self, + address: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult { + let app = self.app.clone(); + tokio::task::spawn_blocking(move || { + app.deposit_blocking( + address, + bitcoin::Amount::from_sat(value_sats), + bitcoin::Amount::from_sat(fee_sats), + ) + .map_err(custom_err) + }) + .await + .unwrap() + } + + async fn create_transfer( + &self, + dest: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult { + let accumulator = + self.app.node.get_tip_accumulator().map_err(custom_err)?; + let tx = self + .app + .wallet + .create_transaction( + &accumulator, + dest, + Amount::from_sat(value_sats), + Amount::from_sat(fee_sats), + ) + .map_err(custom_err)?; + let txid = tx.txid(); + let () = self.app.sign_and_send(tx).map_err(custom_err)?; + Ok(txid) + } + + async fn create_withdrawal( + &self, + mainchain_address: bitcoin::Address, + amount_sats: u64, + fee_sats: u64, + mainchain_fee_sats: u64, + ) -> RpcResult { + let accumulator = + self.app.node.get_tip_accumulator().map_err(custom_err)?; + let tx = self + .app + .wallet + .create_withdrawal( + &accumulator, + mainchain_address, + Amount::from_sat(amount_sats), + Amount::from_sat(mainchain_fee_sats), + Amount::from_sat(fee_sats), + ) + .map_err(custom_err)?; + let txid = tx.txid(); + let () = self.app.sign_and_send(tx).map_err(custom_err)?; + Ok(txid) + } + + async fn format_deposit_address( + &self, + address: Address, + ) -> RpcResult { + let deposit_address = address.format_for_deposit(); + Ok(deposit_address) + } + + async fn generate_mnemonic(&self) -> RpcResult { + let mnemonic = bip39::Mnemonic::new( + bip39::MnemonicType::Words12, + bip39::Language::English, + ); + Ok(mnemonic.to_string()) + } + + async fn get_new_address(&self) -> RpcResult
{ + self.app.wallet.get_new_address().map_err(custom_err) + } + + async fn get_wallet_addresses(&self) -> RpcResult> { + let addrs = self.app.wallet.get_addresses().map_err(custom_err)?; + let mut res: Vec<_> = addrs.into_iter().collect(); + res.sort_by_key(|addr| addr.as_base58()); Ok(res) } - async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()> { - self.app.node.remove_from_mempool(txid).map_err(custom_err) + async fn get_wallet_utxos(&self) -> RpcResult> { + let utxos = self.app.wallet.get_utxos().map_err(custom_err)?; + let utxos = utxos + .into_iter() + .map(|(outpoint, output)| PointedOutput { outpoint, output }) + .collect(); + Ok(utxos) + } + + async fn mine(&self, fee: Option) -> RpcResult<()> { + let fee = fee.map(bitcoin::Amount::from_sat); + self.app + .local_pool + .spawn_pinned({ + let app = self.app.clone(); + move || async move { app.mine(fee).await.map_err(custom_err) } + }) + .await + .unwrap() } async fn set_seed_from_mnemonic(&self, mnemonic: String) -> RpcResult<()> { @@ -321,12 +385,6 @@ impl RpcServer for RpcServerImpl { self.app.wallet.set_seed(&seed_bytes).map_err(custom_err) } - async fn sidechain_wealth_sats(&self) -> RpcResult { - let sidechain_wealth = - self.app.node.get_sidechain_wealth().map_err(custom_err)?; - Ok(sidechain_wealth.to_sat()) - } - async fn sign_transaction( &self, transaction: thunder::types::Transaction, @@ -342,21 +400,6 @@ impl RpcServer for RpcServerImpl { } Ok(authorized) } - - async fn submit_transaction( - &self, - mut transaction: thunder::types::AuthorizedTransaction, - ) -> RpcResult { - let () = self - .app - .submit_transaction(&mut transaction) - .map_err(custom_err)?; - Ok(transaction.transaction.txid()) - } - - async fn stop(&self) { - std::process::exit(0); - } } #[derive(Clone, Debug)] @@ -382,68 +425,133 @@ impl MakeRequestId for RequestIdMaker { } } +pub struct ServerAddesses { + pub _rpc_addr: SocketAddr, + pub _private_rpc_addr: SocketAddr, +} + pub async fn run_server( app: App, + private_rpc_addr: SocketAddr, rpc_addr: SocketAddr, -) -> anyhow::Result { +) -> anyhow::Result { const REQUEST_ID_HEADER: &str = "x-request-id"; // Ordering here matters! Order here is from official docs on request IDs tracings // https://docs.rs/tower-http/latest/tower_http/request_id/index.html#using-trace - let tracer = tower::ServiceBuilder::new() - .layer(SetRequestIdLayer::new( - http::HeaderName::from_static(REQUEST_ID_HEADER), - RequestIdMaker, - )) - .layer( - TraceLayer::new_for_http() - .make_span_with(move |request: &http::Request<_>| { - let request_id = request - .headers() - .get(http::HeaderName::from_static(REQUEST_ID_HEADER)) - .and_then(|h| h.to_str().ok()) - .filter(|s| !s.is_empty()); - - tracing::span!( - tracing::Level::DEBUG, - "request", - method = %request.method(), - uri = %request.uri(), - request_id , // this is needed for the record call below to work + let tracer = || { + tower::ServiceBuilder::new() + .layer(SetRequestIdLayer::new( + http::HeaderName::from_static(REQUEST_ID_HEADER), + RequestIdMaker, + )) + .layer( + TraceLayer::new_for_http() + .make_span_with(move |request: &http::Request<_>| { + let request_id = request + .headers() + .get(http::HeaderName::from_static( + REQUEST_ID_HEADER, + )) + .and_then(|h| h.to_str().ok()) + .filter(|s| !s.is_empty()); + + tracing::span!( + tracing::Level::DEBUG, + "request", + method = %request.method(), + uri = %request.uri(), + request_id , // this is needed for the record call below to work + ) + }) + .on_request(()) + .on_eos(()) + .on_response( + DefaultOnResponse::new().level(tracing::Level::INFO), ) - }) - .on_request(()) - .on_eos(()) - .on_response( - DefaultOnResponse::new().level(tracing::Level::INFO), - ) - .on_failure( - DefaultOnFailure::new().level(tracing::Level::ERROR), - ), - ) - .layer(PropagateRequestIdLayer::new(http::HeaderName::from_static( - REQUEST_ID_HEADER, - ))) - .into_inner(); - - let http_middleware = tower::ServiceBuilder::new() - .layer(tracer) - .layer(CorsLayer::permissive()); - let rpc_middleware = RpcServiceBuilder::new().rpc_logger(1024); + .on_failure( + DefaultOnFailure::new().level(tracing::Level::ERROR), + ), + ) + .layer(PropagateRequestIdLayer::new(http::HeaderName::from_static( + REQUEST_ID_HEADER, + ))) + .into_inner() + }; + + let http_middleware = || { + tower::ServiceBuilder::new() + .layer(tracer()) + .layer(CorsLayer::permissive()) + }; + let rpc_middleware = || RpcServiceBuilder::new().rpc_logger(1024); let server = Server::builder() - .set_http_middleware(http_middleware) - .set_rpc_middleware(rpc_middleware) + .set_http_middleware(http_middleware()) + .set_rpc_middleware(rpc_middleware()) .build(rpc_addr) .await?; - - let addr = server.local_addr()?; - - let handle = server.start(RpcServerImpl { app }.into_rpc()); - - // In this example we don't care about doing shutdown so let's it run forever. - // You may use the `ServerHandle` to shut it down or manage it yourself. - tokio::spawn(handle.stopped()); - - Ok(addr) + let rpc_server_addr = server.local_addr()?; + + let (_task_handle, server_addrs) = if private_rpc_addr != rpc_addr { + let private_rpc_server = Server::builder() + .set_http_middleware(http_middleware()) + .set_rpc_middleware(rpc_middleware()) + .build(private_rpc_addr) + .await?; + let private_rpc_server_addr = private_rpc_server.local_addr()?; + + let rpc_server_handle = { + let rpc_server_impl = RpcServerImpl:: { app: app.clone() }; + let mut rpc_module = + rpc_api::open_api::RpcServer::into_rpc(rpc_server_impl.clone()); + rpc_module + .merge(rpc_api::node::RpcServer::into_rpc(rpc_server_impl))?; + server.start(rpc_module) + }; + let private_only_rpc_server_handle = { + let rpc_server_impl = RpcServerImpl:: { app }; + let mut rpc_module = rpc_api::open_api::RpcServer::into_rpc( + PrivateOnlyRpcServerImpl, + ); + rpc_module.merge(rpc_api::node::PrivateRpcServer::into_rpc( + rpc_server_impl.clone(), + ))?; + rpc_module + .merge(rpc_api::wallet::RpcServer::into_rpc(rpc_server_impl))?; + private_rpc_server.start(rpc_module) + }; + let server_addrs = ServerAddesses { + _rpc_addr: rpc_server_addr, + _private_rpc_addr: private_rpc_server_addr, + }; + let task_handle = tokio::spawn(async { + tokio::select! { + () = rpc_server_handle.stopped() => (), + () = private_only_rpc_server_handle.stopped() => (), + } + }); + (task_handle, server_addrs) + } else { + let rpc_server_impl = RpcServerImpl:: { app }; + let mut rpc_module = + rpc_api::open_api::RpcServer::into_rpc(rpc_server_impl.clone()); + rpc_module.merge(rpc_api::node::PrivateRpcServer::into_rpc( + rpc_server_impl.clone(), + ))?; + rpc_module.merge(rpc_api::node::RpcServer::into_rpc( + rpc_server_impl.clone(), + ))?; + rpc_module + .merge(rpc_api::wallet::RpcServer::into_rpc(rpc_server_impl))?; + + let server_addrs = ServerAddesses { + _rpc_addr: rpc_server_addr, + _private_rpc_addr: rpc_server_addr, + }; + let handle = server.start(rpc_module); + let task_handle = tokio::spawn(handle.stopped()); + (task_handle, server_addrs) + }; + Ok(server_addrs) } diff --git a/cli/lib.rs b/cli/lib.rs index 5dde9dbd..644d8fec 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -5,7 +5,10 @@ use http::HeaderMap; use jsonrpsee::{core::client::ClientT, http_client::HttpClientBuilder}; use thunder::types::{Address, Txid}; -use thunder_app_rpc_api::RpcClient; +use thunder_app_rpc_api::{ + node::{PrivateRpcClient as _, RpcClient as _}, + wallet::RpcClient as _, +}; use tracing_subscriber::layer::SubscriberExt as _; struct JsonParser(PhantomData); @@ -283,9 +286,12 @@ where serde_json::to_string_pretty(&withdrawal_bundle)? } Command::OpenApiSchema => { - let openapi = - ::openapi(); - openapi.to_pretty_json()? + use utoipa::OpenApi as _; + let mut schema = thunder_app_rpc_api::open_api::RpcDoc::openapi(); + schema.merge(thunder_app_rpc_api::node::PrivateRpcDoc::openapi()); + schema.merge(thunder_app_rpc_api::node::RpcDoc::openapi()); + schema.merge(thunder_app_rpc_api::wallet::RpcDoc::openapi()); + schema.to_pretty_json()? } Command::RemoveFromMempool { txid } => { let () = rpc_client.remove_from_mempool(txid).await?; diff --git a/integration_tests/ibd.rs b/integration_tests/ibd.rs index 201ab8c1..e4fec3e8 100644 --- a/integration_tests/ibd.rs +++ b/integration_tests/ibd.rs @@ -12,7 +12,7 @@ use bip300301_enforcer_integration_tests::{ util::{AbortOnDrop, AsyncTrial, TestFailureCollector, TestFileRegistry}, }; use futures::{FutureExt, StreamExt as _, channel::mpsc, future::BoxFuture}; -use thunder_app_rpc_api::RpcClient as _; +use thunder_app_rpc_api::node::{PrivateRpcClient as _, RpcClient as _}; use tokio::time::sleep; use tracing::Instrument as _; diff --git a/integration_tests/integration_test.rs b/integration_tests/integration_test.rs index e0383f87..d3a9b8b2 100644 --- a/integration_tests/integration_test.rs +++ b/integration_tests/integration_test.rs @@ -9,7 +9,7 @@ use bip300301_enforcer_integration_tests::{ }; use bip300301_enforcer_lib::bins::CommandExt; use futures::{FutureExt, channel::mpsc::UnboundedSender, future::BoxFuture}; -use thunder_app_rpc_api::RpcClient as _; +use thunder_app_rpc_api::node::RpcClient as _; use crate::{ ibd::ibd_trial, diff --git a/integration_tests/setup.rs b/integration_tests/setup.rs index b6d76934..01074eb6 100644 --- a/integration_tests/setup.rs +++ b/integration_tests/setup.rs @@ -13,7 +13,7 @@ use futures::{TryFutureExt as _, channel::mpsc, future}; use reserve_port::ReservedPort; use thiserror::Error; use thunder::types::{Network, OutputContent, PointedOutput}; -use thunder_app_rpc_api::RpcClient as _; +use thunder_app_rpc_api::{node::RpcClient as _, wallet::RpcClient as _}; use tokio::time::sleep; use crate::util::ThunderApp; diff --git a/integration_tests/unknown_withdrawal.rs b/integration_tests/unknown_withdrawal.rs index 91d1bb6b..613cce30 100644 --- a/integration_tests/unknown_withdrawal.rs +++ b/integration_tests/unknown_withdrawal.rs @@ -19,7 +19,7 @@ use futures::{ FutureExt as _, StreamExt as _, channel::mpsc, future::BoxFuture, }; use thunder::types::OutPoint; -use thunder_app_rpc_api::RpcClient as _; +use thunder_app_rpc_api::node::RpcClient as _; use tokio::time::sleep; use tracing::Instrument as _; diff --git a/integration_tests/util.rs b/integration_tests/util.rs index a3dedd02..d00fbc18 100644 --- a/integration_tests/util.rs +++ b/integration_tests/util.rs @@ -58,6 +58,8 @@ impl ThunderApp { "--net-addr".to_owned(), format!("127.0.0.1:{}", self.net_port), format!("--network={}", self.network), + "--private-rpc-addr".to_owned(), + format!("127.0.0.1:{}", self.rpc_port), "--rpc-addr".to_owned(), format!("127.0.0.1:{}", self.rpc_port), ]; diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 73ec176d..2da4c806 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -1,239 +1,287 @@ //! RPC API -use std::{collections::HashSet, net::SocketAddr}; - -use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use l2l_openapi::open_api; -use serde::{Deserialize, Serialize}; -use thunder_types::{ - Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, - OutputContent, Pointed, PointedOutput, SpentOutput, Transaction, Txid, - WithdrawalBundle, net::Peer, schema as thunder_schema, wallet::Balance, -}; -use utoipa::ToSchema; - mod schema; -#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] -pub struct GetTransactionResponse { - pub tx: Transaction, - /// Block hash, if in the active chain - pub block_hash: Option, +pub mod open_api { + use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + use l2l_openapi::open_api; + + use crate::schema; + + #[open_api] + #[rpc(client, server)] + pub trait Rpc { + /// Get OpenAPI schema + #[open_api_method(output_schema(PartialSchema = "schema::OpenApi"))] + #[method(name = "openapi_schema")] + async fn openapi_schema(&self) -> RpcResult; + } } -#[open_api(ref_schemas[ - Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, - schema::BitcoinTxid, thunder_schema::BitcoinAddr, - thunder_schema::BitcoinOutPoint, -])] -#[rpc(client, server)] -pub trait Rpc { - /// Get balance in sats - #[open_api_method(output_schema(ToSchema))] - #[method(name = "balance")] - async fn balance(&self) -> RpcResult; - - /// Connect to a peer - #[open_api_method(output_schema(ToSchema))] - #[method(name = "connect_peer")] - async fn connect_peer( - &self, - #[open_api_method_arg(schema( - PartialSchema = "thunder_schema::SocketAddr" +pub mod node { + use std::{collections::HashSet, net::SocketAddr}; + + use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + use l2l_openapi::open_api; + use serde::{Deserialize, Serialize}; + use thunder_types::{ + Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, + OutputContent, Pointed, PointedOutput, SpentOutput, Transaction, Txid, + WithdrawalBundle, net::Peer, schema as thunder_schema, + }; + use utoipa::ToSchema; + + use crate::{open_api, schema}; + + #[open_api(ref_schemas[ + Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, + schema::BitcoinTxid, thunder_schema::BitcoinAddr, + thunder_schema::BitcoinOutPoint, + ])] + #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] + pub trait PrivateRpc { + /// Connect to a peer + #[open_api_method(output_schema(ToSchema))] + #[method(name = "connect_peer")] + async fn connect_peer( + &self, + #[open_api_method_arg(schema( + PartialSchema = "thunder_schema::SocketAddr" + ))] + addr: SocketAddr, + ) -> RpcResult<()>; + + /// Delete peer from known_peers DB. + /// Connections to the peer are not terminated. + #[method(name = "forget_peer")] + async fn forget_peer( + &self, + #[open_api_method_arg(schema( + PartialSchema = "thunder_schema::SocketAddr" + ))] + addr: SocketAddr, + ) -> RpcResult<()>; + + /// Remove a tx from the mempool + #[open_api_method(output_schema(ToSchema))] + #[method(name = "remove_from_mempool")] + async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()>; + + /// Stop the node + #[method(name = "stop")] + async fn stop(&self); + } + + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct GetTransactionResponse { + pub tx: Transaction, + /// Block hash, if in the active chain + pub block_hash: Option, + } + + #[open_api(ref_schemas[ + Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, + schema::BitcoinTxid, thunder_schema::BitcoinAddr, + thunder_schema::BitcoinOutPoint, + ])] + #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] + pub trait Rpc { + /// Get the block with specified block hash, if it exists + #[method(name = "get_block")] + async fn get_block( + &self, + block_hash: thunder_types::BlockHash, + ) -> RpcResult>; + + /// Get mainchain blocks that commit to a specified block hash + #[open_api_method(output_schema( + PartialSchema = "thunder_schema::BitcoinBlockHash" ))] - addr: SocketAddr, - ) -> RpcResult<()>; - - /// Deposit to address - #[open_api_method(output_schema(PartialSchema = "schema::BitcoinTxid"))] - #[method(name = "create_deposit")] - async fn create_deposit( - &self, - address: Address, - value_sats: u64, - fee_sats: u64, - ) -> RpcResult; - - /// Create a tx that transfers funds to the specified address - #[method(name = "create_transfer")] - async fn create_transfer( - &self, - dest: Address, - value_sats: u64, - fee_sats: u64, - ) -> RpcResult; - - /// Creates a tx that initiates a withdrawal to the specified mainchain - /// address - #[method(name = "create_withdrawal")] - async fn create_withdrawal( - &self, - #[open_api_method_arg(schema( - PartialSchema = "thunder_schema::BitcoinAddr" + #[method(name = "get_bmm_inclusions")] + async fn get_bmm_inclusions( + &self, + block_hash: thunder_types::BlockHash, + ) -> RpcResult>; + + /// Get the best mainchain block hash known by Thunder + #[open_api_method(output_schema( + PartialSchema = "schema::Optional" ))] - mainchain_address: bitcoin::Address< - bitcoin::address::NetworkUnchecked, - >, - amount_sats: u64, - fee_sats: u64, - mainchain_fee_sats: u64, - ) -> RpcResult; - - /// Format a deposit address - #[method(name = "format_deposit_address")] - async fn format_deposit_address( - &self, - address: Address, - ) -> RpcResult; - - /// Delete peer from known_peers DB. - /// Connections to the peer are not terminated. - #[method(name = "forget_peer")] - async fn forget_peer( - &self, - #[open_api_method_arg(schema( - PartialSchema = "thunder_schema::SocketAddr" + #[method(name = "get_best_mainchain_block_hash")] + async fn get_best_mainchain_block_hash( + &self, + ) -> RpcResult>; + + /// Get the best sidechain block hash known by Thunder + #[open_api_method(output_schema( + PartialSchema = "schema::Optional" + ))] + #[method(name = "get_best_sidechain_block_hash")] + async fn get_best_sidechain_block_hash( + &self, + ) -> RpcResult>; + + /// Get stxos for addresses + #[method(name = "get_stxos")] + async fn get_stxos( + &self, + addresses: HashSet
, + ) -> RpcResult>>; + + /// Get transaction by txid + #[method(name = "get_transaction")] + async fn get_transaction( + &self, + txid: Txid, + ) -> RpcResult>; + + /// Get utxos for addresses + #[method(name = "get_utxos")] + async fn get_utxos( + &self, + addresses: HashSet
, + ) -> RpcResult>; + + /// Get the current block count + #[method(name = "getblockcount")] + async fn getblockcount(&self) -> RpcResult; + + /// Get the height of the latest failed withdrawal bundle + #[method(name = "latest_failed_withdrawal_bundle_height")] + async fn latest_failed_withdrawal_bundle_height( + &self, + ) -> RpcResult>; + + /// List peers + #[method(name = "list_peers")] + async fn list_peers(&self) -> RpcResult>; + + /// List all UTXOs + #[method(name = "list_utxos")] + async fn list_utxos(&self) -> RpcResult>; + + /// Get pending withdrawal bundle + #[open_api_method(output_schema(ToSchema))] + #[method(name = "pending_withdrawal_bundle")] + async fn pending_withdrawal_bundle( + &self, + ) -> RpcResult>; + + /// Get total sidechain wealth + #[method(name = "sidechain_wealth")] + async fn sidechain_wealth_sats(&self) -> RpcResult; + + /// Verify and broadcast a transaction + #[method(name = "submit_transaction")] + async fn submit_transaction( + &self, + transaction: Authorized, + ) -> RpcResult; + } +} + +pub mod wallet { + use jsonrpsee::{core::RpcResult, proc_macros::rpc}; + use l2l_openapi::open_api; + use thunder_types::{ + Address, Authorized, MerkleRoot, OutPoint, Output, OutputContent, + PointedOutput, Transaction, Txid, schema as thunder_schema, + wallet::Balance, + }; + + use crate::{open_api, schema}; + + #[open_api(ref_schemas[ + Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, + schema::BitcoinTxid, thunder_schema::BitcoinAddr, + thunder_schema::BitcoinOutPoint, + ])] + #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] + pub trait Rpc { + /// Get balance in sats + #[open_api_method(output_schema(ToSchema))] + #[method(name = "balance")] + async fn balance(&self) -> RpcResult; + + /// Deposit to address + #[open_api_method(output_schema( + PartialSchema = "schema::BitcoinTxid" ))] - addr: SocketAddr, - ) -> RpcResult<()>; - - /// Generate a mnemonic seed phrase - #[method(name = "generate_mnemonic")] - async fn generate_mnemonic(&self) -> RpcResult; - - /// Get the block with specified block hash, if it exists - #[method(name = "get_block")] - async fn get_block( - &self, - block_hash: thunder_types::BlockHash, - ) -> RpcResult>; - - /// Get mainchain blocks that commit to a specified block hash - #[open_api_method(output_schema( - PartialSchema = "thunder_schema::BitcoinBlockHash" - ))] - #[method(name = "get_bmm_inclusions")] - async fn get_bmm_inclusions( - &self, - block_hash: thunder_types::BlockHash, - ) -> RpcResult>; - - /// Get the best mainchain block hash known by Thunder - #[open_api_method(output_schema( - PartialSchema = "schema::Optional" - ))] - #[method(name = "get_best_mainchain_block_hash")] - async fn get_best_mainchain_block_hash( - &self, - ) -> RpcResult>; - - /// Get the best sidechain block hash known by Thunder - #[open_api_method(output_schema( - PartialSchema = "schema::Optional" - ))] - #[method(name = "get_best_sidechain_block_hash")] - async fn get_best_sidechain_block_hash( - &self, - ) -> RpcResult>; - - /// Get a new address - #[method(name = "get_new_address")] - async fn get_new_address(&self) -> RpcResult
; - - /// Get stxos for addresses - #[method(name = "get_stxos")] - async fn get_stxos( - &self, - addresses: HashSet
, - ) -> RpcResult>>; - - /// Get transaction by txid - #[method(name = "get_transaction")] - async fn get_transaction( - &self, - txid: Txid, - ) -> RpcResult>; - - /// Get utxos for addresses - #[method(name = "get_utxos")] - async fn get_utxos( - &self, - addresses: HashSet
, - ) -> RpcResult>; - - /// Get wallet addresses, sorted by base58 encoding - #[method(name = "get_wallet_addresses")] - async fn get_wallet_addresses(&self) -> RpcResult>; - - /// Get wallet UTXOs - #[method(name = "get_wallet_utxos")] - async fn get_wallet_utxos(&self) -> RpcResult>; - - /// Get the current block count - #[method(name = "getblockcount")] - async fn getblockcount(&self) -> RpcResult; - - /// Get the height of the latest failed withdrawal bundle - #[method(name = "latest_failed_withdrawal_bundle_height")] - async fn latest_failed_withdrawal_bundle_height( - &self, - ) -> RpcResult>; - - /// List peers - #[method(name = "list_peers")] - async fn list_peers(&self) -> RpcResult>; - - /// List all UTXOs - #[method(name = "list_utxos")] - async fn list_utxos(&self) -> RpcResult>; - - /// Attempt to mine a sidechain block - #[open_api_method(output_schema(ToSchema))] - #[method(name = "mine")] - async fn mine(&self, fee: Option) -> RpcResult<()>; - - /// Get OpenAPI schema - #[open_api_method(output_schema(PartialSchema = "schema::OpenApi"))] - #[method(name = "openapi_schema")] - async fn openapi_schema(&self) -> RpcResult; - - /// Get pending withdrawal bundle - #[open_api_method(output_schema(ToSchema))] - #[method(name = "pending_withdrawal_bundle")] - async fn pending_withdrawal_bundle( - &self, - ) -> RpcResult>; - - /// Remove a tx from the mempool - #[open_api_method(output_schema(ToSchema))] - #[method(name = "remove_from_mempool")] - async fn remove_from_mempool(&self, txid: Txid) -> RpcResult<()>; - - /// Set the wallet seed from a mnemonic seed phrase - #[open_api_method(output_schema(ToSchema))] - #[method(name = "set_seed_from_mnemonic")] - async fn set_seed_from_mnemonic(&self, mnemonic: String) -> RpcResult<()>; - - /// Get total sidechain wealth - #[method(name = "sidechain_wealth")] - async fn sidechain_wealth_sats(&self) -> RpcResult; - - /// Sign a transaction, and optionally broadcast it. - #[method(name = "sign_transaction")] - async fn sign_transaction( - &self, - transaction: Transaction, - broadcast: Option, - ) -> RpcResult>; - - /// Verify and broadcast a transaction - #[method(name = "submit_transaction")] - async fn submit_transaction( - &self, - transaction: Authorized, - ) -> RpcResult; - - /// Stop the node - #[method(name = "stop")] - async fn stop(&self); + #[method(name = "create_deposit")] + async fn create_deposit( + &self, + address: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult; + + /// Create a tx that transfers funds to the specified address + #[method(name = "create_transfer")] + async fn create_transfer( + &self, + dest: Address, + value_sats: u64, + fee_sats: u64, + ) -> RpcResult; + + /// Creates a tx that initiates a withdrawal to the specified mainchain + /// address + #[method(name = "create_withdrawal")] + async fn create_withdrawal( + &self, + #[open_api_method_arg(schema( + PartialSchema = "thunder_schema::BitcoinAddr" + ))] + mainchain_address: bitcoin::Address< + bitcoin::address::NetworkUnchecked, + >, + amount_sats: u64, + fee_sats: u64, + mainchain_fee_sats: u64, + ) -> RpcResult; + + /// Format a deposit address + #[method(name = "format_deposit_address")] + async fn format_deposit_address( + &self, + address: Address, + ) -> RpcResult; + + /// Generate a mnemonic seed phrase + #[method(name = "generate_mnemonic")] + async fn generate_mnemonic(&self) -> RpcResult; + + /// Get a new address + #[method(name = "get_new_address")] + async fn get_new_address(&self) -> RpcResult
; + + /// Get wallet addresses, sorted by base58 encoding + #[method(name = "get_wallet_addresses")] + async fn get_wallet_addresses(&self) -> RpcResult>; + + /// Get wallet UTXOs + #[method(name = "get_wallet_utxos")] + async fn get_wallet_utxos(&self) -> RpcResult>; + + /// Attempt to mine a sidechain block + #[open_api_method(output_schema(ToSchema))] + #[method(name = "mine")] + async fn mine(&self, fee: Option) -> RpcResult<()>; + + /// Set the wallet seed from a mnemonic seed phrase + #[open_api_method(output_schema(ToSchema))] + #[method(name = "set_seed_from_mnemonic")] + async fn set_seed_from_mnemonic( + &self, + mnemonic: String, + ) -> RpcResult<()>; + + /// Sign a transaction, and optionally broadcast it. + #[method(name = "sign_transaction")] + async fn sign_transaction( + &self, + transaction: Transaction, + broadcast: Option, + ) -> RpcResult>; + } } From 59c7f22bc0604a5854ed53b383a3a93d4e9a680d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Olav?= Date: Thu, 6 Aug 2026 06:37:35 +0200 Subject: [PATCH 16/18] RPC: add get_block_template/connect_block methods --- app/app.rs | 68 ++++++++++- app/rpc_server.rs | 47 +++++++- cli/lib.rs | 21 ++++ integration_tests/block_template.rs | 155 ++++++++++++++++++++++++++ integration_tests/integration_test.rs | 6 + integration_tests/main.rs | 1 + rpc-api/lib.rs | 44 +++++++- 7 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 integration_tests/block_template.rs diff --git a/app/app.rs b/app/app.rs index 57dc01a7..01f90199 100644 --- a/app/app.rs +++ b/app/app.rs @@ -94,6 +94,16 @@ struct ProtoSupport { wallet: bool, } +/// A block that is ready to be blind merged mined +pub struct BlockTemplate { + /// Bribe to offer for this block + pub bribe: bitcoin::Amount, + pub header: types::Header, + pub body: types::Body, + /// Fees collected by the transactions in the block + pub fees: bitcoin::Amount, +} + #[derive(Clone)] pub struct App { pub node: Arc, @@ -354,10 +364,11 @@ impl App { const EMPTY_BLOCK_BMM_BRIBE: bitcoin::Amount = bitcoin::Amount::from_sat(1000); - pub async fn mine( + /// Assemble a block to blind merge mine, without requesting BMM for it + async fn build_block_template( &self, fee: Option, - ) -> Result<(), Error> { + ) -> Result { let Some(miner) = self.miner.as_ref() else { return Err(Error::NoCusfMainchainWalletClient); }; @@ -429,7 +440,7 @@ impl App { } else { None }; - let (bribe, header, body) = if prev_side_hash == tip_hash { + let (bribe, header, body, fees) = if prev_side_hash == tip_hash { const NUM_TRANSACTIONS: usize = 1000; let (txs, tx_fees) = self.node.get_transactions(NUM_TRANSACTIONS)?; @@ -484,7 +495,7 @@ impl App { Self::EMPTY_BLOCK_BMM_BRIBE } }); - (bribe, header, body) + (bribe, header, body, tx_fees) } else { let coinbase = Vec::new(); let (merkle_root, roots) = { @@ -523,8 +534,55 @@ impl App { prev_main_hash, }; let bribe = Self::EMPTY_BLOCK_BMM_BRIBE; - (bribe, header, body) + (bribe, header, body, bitcoin::Amount::ZERO) + }; + Ok(BlockTemplate { + bribe, + header, + body, + fees, + }) + } + + /// Assemble a block to blind merge mine. The caller requests BMM for + /// `header.hash()` itself, and submits the block via `connect_block` once + /// its BMM request is included in a mainchain block. + pub async fn get_block_template(&self) -> Result { + self.build_block_template(None).await + } + + /// Connect a block for which a BMM request was included in the specified + /// mainchain block. Returns `true` if it was accepted as the new tip. + pub async fn connect_block( + &self, + block: types::Block, + main_block_hash: bitcoin::BlockHash, + ) -> Result { + let types::Block { header, body } = block; + let accepted = self + .node + .submit_block(main_block_hash, &header, &body) + .await?; + if accepted { + let () = self.update()?; + } + Ok(accepted) + } + + /// Attempt to mine a sidechain block + pub async fn mine( + &self, + fee: Option, + ) -> Result<(), Error> { + let Some(miner) = self.miner.as_ref() else { + return Err(Error::NoCusfMainchainWalletClient); }; + let BlockTemplate { + bribe, + header, + body, + .. + } = self.build_block_template(fee).await?; let mut miner_write = miner.write().await; let bmm_txid = miner_write .attempt_bmm(bribe.to_sat(), 0, header, body) diff --git a/app/rpc_server.rs b/app/rpc_server.rs index d08eb120..08f720c3 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -7,8 +7,8 @@ use jsonrpsee::{ types::ErrorObject, }; use thunder::types::{ - Address, Pointed, PointedOutput, SpentOutput, Txid, WithdrawalBundle, - net::Peer, wallet::Balance, + Address, Block, Pointed, PointedOutput, SpentOutput, Txid, + WithdrawalBundle, net::Peer, wallet::Balance, }; use thunder_app_rpc_api as rpc_api; use tower_http::{ @@ -100,6 +100,25 @@ impl rpc_api::node::PrivateRpcServer for RpcServerImpl { impl rpc_api::node::RpcServer for RpcServerImpl { + async fn connect_block( + &self, + block: Block, + main_block_hash: bitcoin::BlockHash, + ) -> RpcResult { + self.app + .local_pool + .spawn_pinned({ + let app = self.app.clone(); + move || async move { + app.connect_block(block, main_block_hash) + .await + .map_err(custom_err) + } + }) + .await + .unwrap() + } + async fn get_block( &self, block_hash: thunder::types::BlockHash, @@ -342,6 +361,30 @@ impl rpc_api::wallet::RpcServer for RpcServerImpl { Ok(mnemonic.to_string()) } + async fn get_block_template( + &self, + ) -> RpcResult { + let template = self + .app + .local_pool + .spawn_pinned({ + let app = self.app.clone(); + move || async move { + app.get_block_template().await.map_err(custom_err) + } + }) + .await + .unwrap()?; + Ok(rpc_api::wallet::GetBlockTemplateResponse { + critical_hash: template.header.hash(), + block: Block { + header: template.header, + body: template.body, + }, + fees_sats: template.fees.to_sat(), + }) + } + async fn get_new_address(&self) -> RpcResult
{ self.app.wallet.get_new_address().map_err(custom_err) } diff --git a/cli/lib.rs b/cli/lib.rs index 644d8fec..cfaf93fb 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -30,6 +30,12 @@ impl JsonParser { pub enum Command { /// Get balance in sats Balance, + /// Connect a block for which a BMM request was included in the specified + /// mainchain block. The block is the JSON returned by `get-block-template`. + ConnectBlock { + block: String, + main_block_hash: bitcoin::BlockHash, + }, /// Connect to a peer ConnectPeer { addr: SocketAddr }, /// Deposit to address @@ -74,6 +80,8 @@ pub enum Command { GetBlock { block_hash: thunder::types::BlockHash, }, + /// Assemble a block to blind merge mine, without requesting BMM for it + GetBlockTemplate, /// Get mainchain blocks that commit to a specified block hash GetBmmInclusions { block_hash: thunder::types::BlockHash, @@ -168,6 +176,15 @@ where let balance = rpc_client.balance().await?; serde_json::to_string_pretty(&balance)? } + Command::ConnectBlock { + block, + main_block_hash, + } => { + let block = serde_json::from_str(&block)?; + let accepted = + rpc_client.connect_block(block, main_block_hash).await?; + format!("{accepted}") + } Command::ConnectPeer { addr } => { let () = rpc_client.connect_peer(addr).await?; String::default() @@ -227,6 +244,10 @@ where let block_hash = rpc_client.get_best_sidechain_block_hash().await?; serde_json::to_string_pretty(&block_hash)? } + Command::GetBlockTemplate => { + let template = rpc_client.get_block_template().await?; + serde_json::to_string_pretty(&template)? + } Command::GetBmmInclusions { block_hash } => { let bmm_inclusions = rpc_client.get_bmm_inclusions(block_hash).await?; diff --git a/integration_tests/block_template.rs b/integration_tests/block_template.rs new file mode 100644 index 00000000..1b7d21bf --- /dev/null +++ b/integration_tests/block_template.rs @@ -0,0 +1,155 @@ +//! Test assembling block templates + +use bip300301_enforcer_integration_tests::{ + integration_test::{activate_sidechain, fund_enforcer, propose_sidechain}, + setup::{ + Mode, Network, PostSetup as EnforcerPostSetup, + PreSetup as EnforcerPreSetup, SetupOpts as EnforcerSetupOpts, + Sidechain as _, + }, + util::{ + AbortOnDrop, AsyncTrial, BinPaths as EnforcerBinPaths, + TestFailureCollector, TestFileRegistry, + }, +}; +use futures::{ + FutureExt as _, StreamExt as _, channel::mpsc, future::BoxFuture, +}; +use thunder_app_rpc_api::{node::RpcClient as _, wallet::RpcClient as _}; +use tokio::time::sleep; +use tracing::Instrument as _; + +use crate::{ + setup::{Init, PostSetup}, + util::BinPaths, +}; + +/// Initial setup for the test +async fn setup( + enforcer_bin_paths: &EnforcerBinPaths, + res_tx: mpsc::UnboundedSender>, +) -> anyhow::Result { + let enforcer_pre_setup = + EnforcerPreSetup::new(enforcer_bin_paths, Network::Regtest)?; + let mut enforcer_post_setup = { + let setup_opts: EnforcerSetupOpts = Default::default(); + enforcer_pre_setup + .setup(Mode::Mempool, setup_opts, res_tx.clone()) + .await? + }; + let () = propose_sidechain::(&mut enforcer_post_setup).await?; + tracing::info!("Proposed sidechain successfully"); + let () = activate_sidechain::(&mut enforcer_post_setup).await?; + tracing::info!("Activated sidechain successfully"); + let () = fund_enforcer::(&mut enforcer_post_setup).await?; + Ok(enforcer_post_setup) +} + +async fn block_template_task( + bin_paths: BinPaths, + res_tx: mpsc::UnboundedSender>, +) -> anyhow::Result<()> { + let mut enforcer_post_setup = + setup(&bin_paths.others, res_tx.clone()).await?; + let sidechain = PostSetup::setup( + Init { + thunder_app: bin_paths.thunder()?.clone(), + data_dir_suffix: None, + }, + &enforcer_post_setup, + res_tx, + ) + .await?; + tracing::info!("Setup thunder node successfully"); + + tracing::debug!("Checking that the first template is empty"); + let template = sidechain.rpc_client.get_block_template().await?; + anyhow::ensure!(template.block.header.prev_side_hash.is_none()); + anyhow::ensure!(template.block.body.transactions.is_empty()); + anyhow::ensure!(template.fees_sats == 0); + + tracing::debug!("Checking that a template is stable while the chain is"); + let template_repeat = sidechain.rpc_client.get_block_template().await?; + anyhow::ensure!(template_repeat.critical_hash == template.critical_hash); + + tracing::debug!("Checking that a template is not connected by itself"); + anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 0); + + tracing::debug!("BMM 1 block"); + let () = sidechain.bmm_single(&mut enforcer_post_setup).await?; + anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 1); + + tracing::debug!("Checking that a template builds on the new tips"); + let template_connected = sidechain.rpc_client.get_block_template().await?; + let best_side_hash = + sidechain.rpc_client.get_best_sidechain_block_hash().await?; + let best_main_hash = + sidechain.rpc_client.get_best_mainchain_block_hash().await?; + anyhow::ensure!(best_side_hash.is_some()); + anyhow::ensure!( + template_connected.block.header.prev_side_hash == best_side_hash + ); + anyhow::ensure!( + Some(template_connected.block.header.prev_main_hash) == best_main_hash + ); + anyhow::ensure!( + template_connected.critical_hash != template_repeat.critical_hash + ); + + tracing::debug!("Checking that a template commits to its own block"); + anyhow::ensure!( + template_connected.block.header.hash() + == template_connected.critical_hash + ); + + tracing::debug!("Checking that a block without BMM is not connected"); + let connect_without_bmm = sidechain + .rpc_client + .connect_block( + template_connected.block.clone(), + template_connected.block.header.prev_main_hash, + ) + .await; + anyhow::ensure!(matches!(connect_without_bmm, Err(_) | Ok(false))); + anyhow::ensure!(sidechain.rpc_client.getblockcount().await? == 1); + + drop(sidechain); + tracing::info!( + "Removing {}", + enforcer_post_setup.directories.base_dir.path().display() + ); + drop(enforcer_post_setup.tasks); + // Wait for tasks to die + sleep(std::time::Duration::from_secs(1)).await; + enforcer_post_setup.directories.base_dir.cleanup()?; + Ok(()) +} + +async fn block_template(bin_paths: BinPaths) -> anyhow::Result<()> { + let (res_tx, mut res_rx) = mpsc::unbounded(); + let _test_task: AbortOnDrop<()> = tokio::task::spawn({ + let res_tx = res_tx.clone(); + async move { + let res = block_template_task(bin_paths, res_tx.clone()).await; + let _send_err: Result<(), _> = res_tx.unbounded_send(res); + } + .in_current_span() + }) + .into(); + res_rx.next().await.ok_or_else(|| { + anyhow::anyhow!("Unexpected end of test task result stream") + })? +} + +pub fn block_template_trial( + bin_paths: BinPaths, + file_registry: TestFileRegistry, + failure_collector: TestFailureCollector, +) -> AsyncTrial>> { + AsyncTrial::new( + "block_template", + block_template(bin_paths).boxed(), + file_registry, + failure_collector, + ) +} diff --git a/integration_tests/integration_test.rs b/integration_tests/integration_test.rs index d3a9b8b2..5ff99bab 100644 --- a/integration_tests/integration_test.rs +++ b/integration_tests/integration_test.rs @@ -12,6 +12,7 @@ use futures::{FutureExt, channel::mpsc::UnboundedSender, future::BoxFuture}; use thunder_app_rpc_api::node::RpcClient as _; use crate::{ + block_template::block_template_trial, ibd::ibd_trial, setup::{Init, PostSetup}, unknown_withdrawal::unknown_withdrawal_trial, @@ -162,6 +163,11 @@ pub fn tests( failure_collector: TestFailureCollector, ) -> Vec>>> { vec![ + block_template_trial( + bin_paths.clone(), + file_registry.clone(), + failure_collector.clone(), + ), deposit_withdraw_roundtrip_trial( bin_paths.clone(), file_registry.clone(), diff --git a/integration_tests/main.rs b/integration_tests/main.rs index 15dcd867..03a29151 100644 --- a/integration_tests/main.rs +++ b/integration_tests/main.rs @@ -4,6 +4,7 @@ use bip300301_enforcer_integration_tests::util::{ use clap::Parser; use tracing_subscriber::{filter as tracing_filter, layer::SubscriberExt}; +mod block_template; mod ibd; mod integration_test; mod setup; diff --git a/rpc-api/lib.rs b/rpc-api/lib.rs index 2da4c806..3d764420 100644 --- a/rpc-api/lib.rs +++ b/rpc-api/lib.rs @@ -25,7 +25,7 @@ pub mod node { use l2l_openapi::open_api; use serde::{Deserialize, Serialize}; use thunder_types::{ - Address, Authorized, BlockHash, MerkleRoot, OutPoint, Output, + Address, Authorized, Block, BlockHash, MerkleRoot, OutPoint, Output, OutputContent, Pointed, PointedOutput, SpentOutput, Transaction, Txid, WithdrawalBundle, net::Peer, schema as thunder_schema, }; @@ -86,6 +86,20 @@ pub mod node { ])] #[rpc(client, server, server_bounds(Self: open_api::RpcServer))] pub trait Rpc { + /// Connect a block template for which a BMM request was included in the + /// specified mainchain block. Returns `true` if it was accepted as the new + /// tip. + #[open_api_method(output_schema(ToSchema))] + #[method(name = "connect_block")] + async fn connect_block( + &self, + block: Block, + #[open_api_method_arg(schema( + PartialSchema = "thunder_schema::BitcoinBlockHash" + ))] + main_block_hash: bitcoin::BlockHash, + ) -> RpcResult; + /// Get the block with specified block hash, if it exists #[method(name = "get_block")] async fn get_block( @@ -183,14 +197,27 @@ pub mod node { pub mod wallet { use jsonrpsee::{core::RpcResult, proc_macros::rpc}; use l2l_openapi::open_api; + use serde::{Deserialize, Serialize}; use thunder_types::{ - Address, Authorized, MerkleRoot, OutPoint, Output, OutputContent, - PointedOutput, Transaction, Txid, schema as thunder_schema, - wallet::Balance, + Address, Authorized, Block, BlockHash, MerkleRoot, OutPoint, Output, + OutputContent, PointedOutput, Transaction, Txid, + schema as thunder_schema, wallet::Balance, }; + use utoipa::ToSchema; use crate::{open_api, schema}; + #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] + pub struct GetBlockTemplateResponse { + /// Block hash to commit to in a BMM request + pub critical_hash: BlockHash, + /// Block to pass to `connect_block` once its BMM request is included in a + /// mainchain block + pub block: Block, + /// Fees collected by the transactions in the block, in sats + pub fees_sats: u64, + } + #[open_api(ref_schemas[ Address, MerkleRoot, OutPoint, Output, OutputContent, Txid, schema::BitcoinTxid, thunder_schema::BitcoinAddr, @@ -251,6 +278,15 @@ pub mod wallet { #[method(name = "generate_mnemonic")] async fn generate_mnemonic(&self) -> RpcResult; + /// Assemble a block to blind merge mine, without requesting BMM for it. + /// The caller requests BMM for `critical_hash` itself, then passes the + /// block back to `connect_block`. + #[open_api_method(output_schema(ToSchema))] + #[method(name = "get_block_template")] + async fn get_block_template( + &self, + ) -> RpcResult; + /// Get a new address #[method(name = "get_new_address")] async fn get_new_address(&self) -> RpcResult
; From b495c9c7f558cfab58ac839d0b18f148e97a505c Mon Sep 17 00:00:00 2001 From: Ash Manning Date: Fri, 7 Aug 2026 01:34:52 +0800 Subject: [PATCH 17/18] Bump version for release 0.17.2 --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 766a5f13..45013614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6404,7 +6404,7 @@ dependencies = [ [[package]] name = "thunder" -version = "0.17.1" +version = "0.17.2" dependencies = [ "anyhow", "async-lock", @@ -6454,7 +6454,7 @@ dependencies = [ [[package]] name = "thunder_app" -version = "0.17.1" +version = "0.17.2" dependencies = [ "anyhow", "bincode", @@ -6497,7 +6497,7 @@ dependencies = [ [[package]] name = "thunder_app_cli" -version = "0.17.1" +version = "0.17.2" dependencies = [ "anyhow", "bitcoin", @@ -6519,7 +6519,7 @@ dependencies = [ [[package]] name = "thunder_app_rpc_api" -version = "0.17.1" +version = "0.17.2" dependencies = [ "bitcoin", "jsonrpsee", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "thunder_integration_tests" -version = "0.17.1" +version = "0.17.2" dependencies = [ "anyhow", "bip300301_enforcer_integration_tests", @@ -6555,7 +6555,7 @@ dependencies = [ [[package]] name = "thunder_types" -version = "0.17.1" +version = "0.17.2" dependencies = [ "anyhow", "bitcoin", diff --git a/Cargo.toml b/Cargo.toml index 5dd6977c..8ad961e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ edition = "2024" license-file = "LICENSE.txt" publish = false -version = "0.17.1" +version = "0.17.2" [workspace.dependencies] anyhow = "1.0.72" From 6c480c2ecbd295f2981f31b40cc463af4d02c8f4 Mon Sep 17 00:00:00 2001 From: ekulkisnek Date: Fri, 7 Aug 2026 18:42:37 +0000 Subject: [PATCH 18/18] security: require cookie bearer auth on private/wallet RPC Unauthenticated private RPC exposes stop, seed import, transfers, and wallet control to anyone who can reach the port. Generate a mode-0600 cookie at /.cookie and require Authorization: Bearer on the private (or combined) RPC server. - Add rpc_auth cookie load/create helpers + unit tests - Wire ValidateRequestHeaderLayer on private/combined servers - CLI/integration tests/GUI pick up cookie or THUNDER_RPC_AUTH_TOKEN --- Cargo.lock | 4 + app/Cargo.toml | 4 +- app/cli.rs | 14 ++- app/gui/console_logs.rs | 2 + app/main.rs | 11 ++- app/rpc_auth.rs | 183 +++++++++++++++++++++++++++++++++++ app/rpc_server.rs | 70 +++++++++++--- cli/Cargo.toml | 3 +- cli/lib.rs | 69 ++++++++++++- integration_tests/Cargo.toml | 3 +- integration_tests/setup.rs | 28 ++++++ 11 files changed, 366 insertions(+), 25 deletions(-) create mode 100644 app/rpc_auth.rs diff --git a/Cargo.lock b/Cargo.lock index 45013614..b4464beb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6502,6 +6502,7 @@ dependencies = [ "anyhow", "bitcoin", "clap", + "dirs", "http", "jsonrpsee", "serde", @@ -6541,6 +6542,7 @@ dependencies = [ "clap", "dotenvy", "futures", + "http", "jsonrpsee", "libtest-mimic", "reserve-port", @@ -6877,12 +6879,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "base64 0.22.1", "bitflags 2.13.1", "bytes", "futures-util", "http", "http-body", "iri-string", + "mime", "pin-project-lite", "tower", "tower-layer", diff --git a/app/Cargo.toml b/app/Cargo.toml index 45a14668..2dfe0ec8 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -36,13 +36,13 @@ tonic = { workspace = true } tonic-health = { workspace = true } # needs to line up with jsonrpsee tower version... tower = { workspace = true } -tower-http = { workspace = true, features = ["cors", "request-id", "trace"] } +tower-http = { workspace = true, features = ["auth", "cors", "request-id", "trace", "validate-request"] } tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } url = { workspace = true } utoipa = { workspace = true } -uuid = { workspace = true } +uuid = { workspace = true, features = ["v4"] } [dependencies.eframe] workspace = true diff --git a/app/cli.rs b/app/cli.rs index 7a443656..f03b18a4 100644 --- a/app/cli.rs +++ b/app/cli.rs @@ -144,6 +144,11 @@ pub(super) struct Cli { /// Socket address to host the private RPC server #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] private_rpc_addr: SocketAddr, + /// Path to the private-RPC auth cookie file. + /// Defaults to `/.cookie`. Created automatically with mode 0600 + /// if it does not exist. Clients must send `Authorization: Bearer `. + #[arg(long)] + rpc_cookie_file: Option, /// Socket address to host the RPC server #[arg(default_value_t = DEFAULT_RPC_ADDR, long, short)] rpc_addr: SocketAddr, @@ -163,6 +168,8 @@ pub struct Config { pub network: Network, pub network_magic_override: Option, pub private_rpc_addr: SocketAddr, + /// Cookie file used to authenticate private / wallet RPC requests. + pub rpc_cookie_file: PathBuf, pub rpc_addr: SocketAddr, } @@ -189,8 +196,12 @@ impl Cli { } else { saturating_pred_level(self.log_level) }; + let datadir = self.datadir.0; + let rpc_cookie_file = self + .rpc_cookie_file + .unwrap_or_else(|| crate::rpc_auth::default_cookie_path(&datadir)); Ok(Config { - datadir: self.datadir.0, + datadir, headless: self.headless, log_dir, log_level, @@ -201,6 +212,7 @@ impl Cli { network: self.network, network_magic_override: self.network_magic, private_rpc_addr: self.private_rpc_addr, + rpc_cookie_file, rpc_addr: self.rpc_addr, }) } diff --git a/app/gui/console_logs.rs b/app/gui/console_logs.rs index ffc72b55..9e80f708 100644 --- a/app/gui/console_logs.rs +++ b/app/gui/console_logs.rs @@ -69,6 +69,8 @@ impl ConsoleLogs { }; let cli = thunder_app_cli_lib::Cli { rpc_url: self.rpc_addr.clone(), + rpc_auth_token: None, + rpc_cookie_file: None, timeout: None, command, verbose: false, diff --git a/app/main.rs b/app/main.rs index 2515efd8..aa8fe9df 100644 --- a/app/main.rs +++ b/app/main.rs @@ -11,6 +11,7 @@ mod app; mod cli; mod gui; mod line_buffer; +mod rpc_auth; mod rpc_server; mod util; @@ -217,12 +218,16 @@ fn main() -> anyhow::Result<()> { // spawn rpc server app.runtime.spawn({ let app = app.clone(); + let private_rpc_addr = config.private_rpc_addr; + let rpc_addr = config.rpc_addr; + let rpc_cookie_file = config.rpc_cookie_file.clone(); async move { - tracing::info!("starting RPC server at `{}`", config.rpc_addr); + tracing::info!("starting RPC server at `{}`", rpc_addr); if let Err(err) = rpc_server::run_server( app, - config.private_rpc_addr, - config.rpc_addr, + private_rpc_addr, + rpc_addr, + &rpc_cookie_file, ) .await { diff --git a/app/rpc_auth.rs b/app/rpc_auth.rs new file mode 100644 index 00000000..17859404 --- /dev/null +++ b/app/rpc_auth.rs @@ -0,0 +1,183 @@ +//! Cookie-file authentication for the private / wallet RPC surface. +//! +//! On first start, the node writes a randomly generated bearer token to +//! `/.cookie` (mode `0600`). Clients must send: +//! +//! ```text +//! Authorization: Bearer +//! ``` +//! +//! This mirrors Bitcoin Core's cookie-auth model and closes the unauthenticated +//! wallet / control RPC attack surface when the private RPC port is reachable. + +use std::{ + fs::{self, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +/// Default filename for the RPC auth cookie, relative to the data directory. +pub const DEFAULT_COOKIE_FILENAME: &str = ".cookie"; + +/// Default cookie path under the node data directory. +pub fn default_cookie_path(datadir: &Path) -> PathBuf { + datadir.join(DEFAULT_COOKIE_FILENAME) +} + +/// Read an existing cookie token from `path`. +pub fn read_cookie(path: &Path) -> anyhow::Result { + let content = fs::read_to_string(path).map_err(|err| { + anyhow::anyhow!( + "failed to read RPC cookie file `{}`: {err}", + path.display() + ) + })?; + let token = content.trim(); + anyhow::ensure!( + !token.is_empty(), + "RPC cookie file `{}` is empty", + path.display() + ); + // Reject accidental multi-line / corrupted cookies. + anyhow::ensure!( + !token.chars().any(|c| c.is_whitespace()), + "RPC cookie file `{}` contains whitespace; regenerate it", + path.display() + ); + Ok(token.to_owned()) +} + +/// Load the cookie token from `path`, creating a new random cookie if missing. +/// +/// The cookie file is created with mode `0600` so only the node user can read +/// the token. Callers should treat the returned token as a secret. +pub fn load_or_create_cookie(path: &Path) -> anyhow::Result { + if path.exists() { + return read_cookie(path); + } + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|err| { + anyhow::anyhow!( + "failed to create RPC cookie directory `{}`: {err}", + parent.display() + ) + })?; + } + + // 32 bytes of entropy encoded as 64 hex characters (two UUID v4s). + let token = { + let a = uuid::Uuid::new_v4(); + let b = uuid::Uuid::new_v4(); + format!("{}{}", a.as_simple(), b.as_simple()) + }; + + write_cookie(path, &token)?; + tracing::info!( + path = %path.display(), + "wrote new private-RPC auth cookie (mode 0600)" + ); + Ok(token) +} + +/// Write `token` to `path` with mode `0600`, replacing any existing file. +fn write_cookie(path: &Path, token: &str) -> anyhow::Result<()> { + // Write via a temp file then rename so a crash cannot leave an empty + // cookie that subsequent starts would treat as "exists but empty". + let tmp_path = path.with_extension("cookie.tmp"); + + { + let mut opts = OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut file = opts.open(&tmp_path).map_err(|err| { + anyhow::anyhow!( + "failed to create RPC cookie file `{}`: {err}", + tmp_path.display() + ) + })?; + file.write_all(token.as_bytes()).map_err(|err| { + anyhow::anyhow!( + "failed to write RPC cookie file `{}`: {err}", + tmp_path.display() + ) + })?; + file.sync_all().map_err(|err| { + anyhow::anyhow!( + "failed to sync RPC cookie file `{}`: {err}", + tmp_path.display() + ) + })?; + } + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = fs::Permissions::from_mode(0o600); + fs::set_permissions(&tmp_path, perms).map_err(|err| { + anyhow::anyhow!( + "failed to set permissions on RPC cookie file `{}`: {err}", + tmp_path.display() + ) + })?; + } + + fs::rename(&tmp_path, path).map_err(|err| { + anyhow::anyhow!( + "failed to install RPC cookie file `{}`: {err}", + path.display() + ) + })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_cookie_path(label: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("thunder_rpc_cookie_{label}_{nanos}")) + } + + #[test] + fn load_or_create_writes_and_reloads_same_token() { + let path = temp_cookie_path("roundtrip"); + drop(fs::remove_file(&path)); + + let first = load_or_create_cookie(&path).expect("create cookie"); + assert_eq!(first.len(), 64); + assert!(path.is_file()); + + let second = load_or_create_cookie(&path).expect("reload cookie"); + assert_eq!(first, second); + + drop(fs::remove_file(&path)); + } + + #[test] + fn read_cookie_rejects_empty_file() { + let path = temp_cookie_path("empty"); + fs::write(&path, " ").unwrap(); + let err = read_cookie(&path).unwrap_err().to_string(); + assert!(err.contains("empty"), "err={err}"); + drop(fs::remove_file(&path)); + } + + #[test] + fn read_cookie_rejects_whitespace_token() { + let path = temp_cookie_path("ws"); + fs::write(&path, "abc def").unwrap(); + let err = read_cookie(&path).unwrap_err().to_string(); + assert!(err.contains("whitespace"), "err={err}"); + drop(fs::remove_file(&path)); + } +} diff --git a/app/rpc_server.rs b/app/rpc_server.rs index 08f720c3..226b55ad 100644 --- a/app/rpc_server.rs +++ b/app/rpc_server.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, net::SocketAddr}; +use std::{collections::HashSet, net::SocketAddr, path::Path}; use bitcoin::Amount; use jsonrpsee::{ @@ -17,9 +17,10 @@ use tower_http::{ MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer, }, trace::{DefaultOnFailure, DefaultOnResponse, TraceLayer}, + validate_request::ValidateRequestHeaderLayer, }; -use crate::app::App; +use crate::{app::App, rpc_auth}; fn custom_err_msg(err_msg: impl Into) -> ErrorObject<'static> { ErrorObject::owned(-1, err_msg.into(), Option::<()>::None) @@ -477,9 +478,22 @@ pub async fn run_server( app: App, private_rpc_addr: SocketAddr, rpc_addr: SocketAddr, + rpc_cookie_file: &Path, ) -> anyhow::Result { const REQUEST_ID_HEADER: &str = "x-request-id"; + // Private / wallet RPC is always authenticated via a bearer token stored in + // the cookie file. When public and private share a single bind address the + // whole surface is authenticated (wallet methods are exposed there). + let auth_token = rpc_auth::load_or_create_cookie(rpc_cookie_file)?; + if !private_rpc_addr.ip().is_loopback() { + tracing::warn!( + %private_rpc_addr, + "private RPC is bound to a non-loopback address; ensure the cookie \ + file permissions stay restrictive and the token is not shared" + ); + } + // Ordering here matters! Order here is from official docs on request IDs tracings // https://docs.rs/tower-http/latest/tower_http/request_id/index.html#using-trace let tracer = || { @@ -522,23 +536,38 @@ pub async fn run_server( .into_inner() }; - let http_middleware = || { + let public_http_middleware = || { tower::ServiceBuilder::new() .layer(tracer()) .layer(CorsLayer::permissive()) }; + // Bearer auth is applied only to the private / combined RPC server. + // tower-http marks the helper deprecated as "too basic", but cookie bearer + // auth is exactly the threat model we want here (shared secret on loopback). + #[allow(deprecated)] + let private_http_middleware = { + let auth_token = auth_token.clone(); + move || { + tower::ServiceBuilder::new() + .layer(tracer()) + .layer(CorsLayer::permissive()) + .layer(ValidateRequestHeaderLayer::bearer(&auth_token)) + } + }; let rpc_middleware = || RpcServiceBuilder::new().rpc_logger(1024); - let server = Server::builder() - .set_http_middleware(http_middleware()) - .set_rpc_middleware(rpc_middleware()) - .build(rpc_addr) - .await?; - let rpc_server_addr = server.local_addr()?; - let (_task_handle, server_addrs) = if private_rpc_addr != rpc_addr { + // Public read-mostly surface: no auth required. + let server = Server::builder() + .set_http_middleware(public_http_middleware()) + .set_rpc_middleware(rpc_middleware()) + .build(rpc_addr) + .await?; + let rpc_server_addr = server.local_addr()?; + + // Private / wallet / control surface: cookie auth required. let private_rpc_server = Server::builder() - .set_http_middleware(http_middleware()) + .set_http_middleware(private_http_middleware()) .set_rpc_middleware(rpc_middleware()) .build(private_rpc_addr) .await?; @@ -564,6 +593,11 @@ pub async fn run_server( .merge(rpc_api::wallet::RpcServer::into_rpc(rpc_server_impl))?; private_rpc_server.start(rpc_module) }; + tracing::info!( + %private_rpc_server_addr, + cookie = %rpc_cookie_file.display(), + "private RPC requires Authorization: Bearer " + ); let server_addrs = ServerAddesses { _rpc_addr: rpc_server_addr, _private_rpc_addr: private_rpc_server_addr, @@ -576,6 +610,15 @@ pub async fn run_server( }); (task_handle, server_addrs) } else { + // Combined public+private surface on one port: auth required for all + // methods so wallet / stop cannot be reached anonymously. + let server = Server::builder() + .set_http_middleware(private_http_middleware()) + .set_rpc_middleware(rpc_middleware()) + .build(rpc_addr) + .await?; + let rpc_server_addr = server.local_addr()?; + let rpc_server_impl = RpcServerImpl:: { app }; let mut rpc_module = rpc_api::open_api::RpcServer::into_rpc(rpc_server_impl.clone()); @@ -588,6 +631,11 @@ pub async fn run_server( rpc_module .merge(rpc_api::wallet::RpcServer::into_rpc(rpc_server_impl))?; + tracing::info!( + %rpc_server_addr, + cookie = %rpc_cookie_file.display(), + "combined RPC requires Authorization: Bearer " + ); let server_addrs = ServerAddesses { _rpc_addr: rpc_server_addr, _private_rpc_addr: rpc_server_addr, diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f4fda8df..3328b179 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,7 +9,8 @@ version.workspace = true [dependencies] anyhow = { workspace = true } bitcoin = { workspace = true, features = ["serde"] } -clap = { workspace = true, features = ["derive"] } +clap = { workspace = true, features = ["derive", "env"] } +dirs = { workspace = true } http = { workspace = true } jsonrpsee = { workspace = true, features = ["http-client"] } serde = { workspace = true } diff --git a/cli/lib.rs b/cli/lib.rs index cfaf93fb..f97f5a8e 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, net::SocketAddr, time::Duration}; +use std::{marker::PhantomData, net::SocketAddr, path::PathBuf, time::Duration}; use clap::{Parser, Subcommand}; use http::HeaderMap; @@ -154,6 +154,16 @@ pub struct Cli { #[arg(default_value = "http://localhost:6009", long)] pub rpc_url: url::Url, + /// Bearer token for private / wallet RPC authentication. + /// If unset, the token is read from `--rpc-cookie-file` when present. + #[arg(long, env = "THUNDER_RPC_AUTH_TOKEN")] + pub rpc_auth_token: Option, + + /// Path to the node RPC cookie file (default: platform data dir + /// `thunder/.cookie`). Used when `--rpc-auth-token` is not set. + #[arg(long)] + pub rpc_cookie_file: Option, + #[arg(long, help = "Timeout for RPC requests in seconds (default: 60)")] pub timeout: Option, @@ -358,6 +368,43 @@ fn set_tracing_subscriber() -> anyhow::Result<()> { } impl Cli { + /// Resolve the bearer token used for private RPC auth, if available. + fn resolve_auth_token(&self) -> anyhow::Result> { + if let Some(token) = &self.rpc_auth_token { + anyhow::ensure!( + !token.trim().is_empty(), + "--rpc-auth-token / THUNDER_RPC_AUTH_TOKEN must not be empty" + ); + return Ok(Some(token.trim().to_owned())); + } + + let cookie_path = if let Some(path) = &self.rpc_cookie_file { + path.clone() + } else if let Some(data_dir) = dirs::data_dir() { + data_dir.join("thunder").join(".cookie") + } else { + return Ok(None); + }; + + if !cookie_path.is_file() { + return Ok(None); + } + + let content = std::fs::read_to_string(&cookie_path).map_err(|err| { + anyhow::anyhow!( + "failed to read RPC cookie file `{}`: {err}", + cookie_path.display() + ) + })?; + let token = content.trim(); + anyhow::ensure!( + !token.is_empty(), + "RPC cookie file `{}` is empty", + cookie_path.display() + ); + Ok(Some(token.to_owned())) + } + pub async fn run(self) -> anyhow::Result { if self.verbose { set_tracing_subscriber()?; @@ -369,6 +416,21 @@ impl Cli { tracing::info!("request ID: {}", request_id); + let mut headers = HeaderMap::from_iter([( + http::header::HeaderName::from_static("x-request-id"), + http::header::HeaderValue::from_str(&request_id)?, + )]); + + if let Some(token) = self.resolve_auth_token()? { + let value = format!("Bearer {token}"); + headers.insert( + http::header::AUTHORIZATION, + http::header::HeaderValue::from_str(&value).map_err(|err| { + anyhow::anyhow!("invalid RPC auth token for Authorization header: {err}") + })?, + ); + } + let builder = HttpClientBuilder::default() .request_timeout(Duration::from_secs( self.timeout.unwrap_or(DEFAULT_TIMEOUT), @@ -377,10 +439,7 @@ impl Cli { jsonrpsee::core::middleware::RpcServiceBuilder::new() .rpc_logger(1024), ) - .set_headers(HeaderMap::from_iter([( - http::header::HeaderName::from_static("x-request-id"), - http::header::HeaderValue::from_str(&request_id)?, - )])); + .set_headers(headers); let client = builder.build(self.rpc_url)?; let result = handle_command(&client, self.command).await?; diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index f15ff2ff..a6c31302 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -14,6 +14,7 @@ bitcoin = { workspace = true } clap = { workspace = true } dotenvy = { workspace = true } futures = { workspace = true } +http = { workspace = true } jsonrpsee = { workspace = true } libtest-mimic = { workspace = true } reserve-port = { workspace = true } @@ -38,5 +39,3 @@ workspace = true name = "integration_tests" path = "main.rs" harness = false -test = false -crate-type = ["bin"] \ No newline at end of file diff --git a/integration_tests/setup.rs b/integration_tests/setup.rs index 01074eb6..a0bcd3e8 100644 --- a/integration_tests/setup.rs +++ b/integration_tests/setup.rs @@ -49,8 +49,14 @@ pub enum BmmError { #[derive(Debug, Error)] pub enum SetupError { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), #[error("Failed to create thunder dir")] CreateThunderDir(#[source] std::io::Error), + #[error("Invalid RPC auth header")] + InvalidAuthHeader(#[from] http::header::InvalidHeaderValue), + #[error(transparent)] + Io(#[from] std::io::Error), #[error(transparent)] ReservePort(#[from] reserve_port::Error), #[error(transparent)] @@ -179,7 +185,29 @@ impl Sidechain for PostSetup { }); tracing::debug!("Started thunder"); sleep(Duration::from_secs(1)).await; + + // Combined public+private RPC requires the cookie bearer token. + let cookie_path = thunder_app.data_dir.join(".cookie"); + let cookie = std::fs::read_to_string(&cookie_path).map_err(|err| { + SetupError::Anyhow(anyhow::anyhow!( + "failed to read thunder RPC cookie `{}`: {err}", + cookie_path.display() + )) + })?; + let token = cookie.trim(); + if token.is_empty() { + return Err(SetupError::Anyhow(anyhow::anyhow!( + "thunder RPC cookie `{}` is empty", + cookie_path.display() + ))); + } + let mut headers = http::HeaderMap::new(); + headers.insert( + http::header::AUTHORIZATION, + http::header::HeaderValue::from_str(&format!("Bearer {token}"))?, + ); let rpc_client = jsonrpsee::http_client::HttpClient::builder() + .set_headers(headers) .build(format!("http://127.0.0.1:{}", reserved_ports.rpc.port()))?; tracing::debug!("Generating mnemonic seed phrase"); let mnemonic = rpc_client.generate_mnemonic().await?;